summaryrefslogtreecommitdiff
path: root/kolab.org/www/drupal-7.26/modules/search
diff options
context:
space:
mode:
Diffstat (limited to 'kolab.org/www/drupal-7.26/modules/search')
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search-block-form.tpl.php37
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search-result.tpl.php81
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search-results.tpl.php35
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search-rtl.css13
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.admin.inc186
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.api.php376
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.css34
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.extender.inc536
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.info15
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.install182
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.module1356
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.pages.inc158
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/search.test2079
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/tests/UnicodeTest.txt333
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.info12
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.module70
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.info12
-rw-r--r--kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.module69
18 files changed, 5584 insertions, 0 deletions
diff --git a/kolab.org/www/drupal-7.26/modules/search/search-block-form.tpl.php b/kolab.org/www/drupal-7.26/modules/search/search-block-form.tpl.php
new file mode 100644
index 0000000..da58403
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search-block-form.tpl.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Displays the search form block.
+ *
+ * Available variables:
+ * - $search_form: The complete search form ready for print.
+ * - $search: Associative array of search elements. Can be used to print each
+ * form element separately.
+ *
+ * Default elements within $search:
+ * - $search['search_block_form']: Text input area wrapped in a div.
+ * - $search['actions']: Rendered form buttons.
+ * - $search['hidden']: Hidden form elements. Used to validate forms when
+ * submitted.
+ *
+ * Modules can add to the search form, so it is recommended to check for their
+ * existence before printing. The default keys will always exist. To check for
+ * a module-provided field, use code like this:
+ * @code
+ * <?php if (isset($search['extra_field'])): ?>
+ * <div class="extra-field">
+ * <?php print $search['extra_field']; ?>
+ * </div>
+ * <?php endif; ?>
+ * @endcode
+ *
+ * @see template_preprocess_search_block_form()
+ */
+?>
+<div class="container-inline">
+ <?php if (empty($variables['form']['#block']->subject)): ?>
+ <h2 class="element-invisible"><?php print t('Search form'); ?></h2>
+ <?php endif; ?>
+ <?php print $search_form; ?>
+</div>
diff --git a/kolab.org/www/drupal-7.26/modules/search/search-result.tpl.php b/kolab.org/www/drupal-7.26/modules/search/search-result.tpl.php
new file mode 100644
index 0000000..5f2e8bd
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search-result.tpl.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation for displaying a single search result.
+ *
+ * This template renders a single search result and is collected into
+ * search-results.tpl.php. This and the parent template are
+ * dependent to one another sharing the markup for definition lists.
+ *
+ * Available variables:
+ * - $url: URL of the result.
+ * - $title: Title of the result.
+ * - $snippet: A small preview of the result. Does not apply to user searches.
+ * - $info: String of all the meta information ready for print. Does not apply
+ * to user searches.
+ * - $info_split: Contains same data as $info, split into a keyed array.
+ * - $module: The machine-readable name of the module (tab) being searched, such
+ * as "node" or "user".
+ * - $title_prefix (array): An array containing additional output populated by
+ * modules, intended to be displayed in front of the main title tag that
+ * appears in the template.
+ * - $title_suffix (array): An array containing additional output populated by
+ * modules, intended to be displayed after the main title tag that appears in
+ * the template.
+ *
+ * Default keys within $info_split:
+ * - $info_split['module']: The module that implemented the search query.
+ * - $info_split['user']: Author of the node linked to users profile. Depends
+ * on permission.
+ * - $info_split['date']: Last update of the node. Short formatted.
+ * - $info_split['comment']: Number of comments output as "% comments", %
+ * being the count. Depends on comment.module.
+ *
+ * Other variables:
+ * - $classes_array: Array of HTML class attribute values. It is flattened
+ * into a string within the variable $classes.
+ * - $title_attributes_array: Array of HTML attributes for the title. It is
+ * flattened into a string within the variable $title_attributes.
+ * - $content_attributes_array: Array of HTML attributes for the content. It is
+ * flattened into a string within the variable $content_attributes.
+ *
+ * Since $info_split is keyed, a direct print of the item is possible.
+ * This array does not apply to user searches so it is recommended to check
+ * for its existence before printing. The default keys of 'type', 'user' and
+ * 'date' always exist for node searches. Modules may provide other data.
+ * @code
+ * <?php if (isset($info_split['comment'])): ?>
+ * <span class="info-comment">
+ * <?php print $info_split['comment']; ?>
+ * </span>
+ * <?php endif; ?>
+ * @endcode
+ *
+ * To check for all available data within $info_split, use the code below.
+ * @code
+ * <?php print '<pre>'. check_plain(print_r($info_split, 1)) .'</pre>'; ?>
+ * @endcode
+ *
+ * @see template_preprocess()
+ * @see template_preprocess_search_result()
+ * @see template_process()
+ *
+ * @ingroup themeable
+ */
+?>
+<li class="<?php print $classes; ?>"<?php print $attributes; ?>>
+ <?php print render($title_prefix); ?>
+ <h3 class="title"<?php print $title_attributes; ?>>
+ <a href="<?php print $url; ?>"><?php print $title; ?></a>
+ </h3>
+ <?php print render($title_suffix); ?>
+ <div class="search-snippet-info">
+ <?php if ($snippet): ?>
+ <p class="search-snippet"<?php print $content_attributes; ?>><?php print $snippet; ?></p>
+ <?php endif; ?>
+ <?php if ($info): ?>
+ <p class="search-info"><?php print $info; ?></p>
+ <?php endif; ?>
+ </div>
+</li>
diff --git a/kolab.org/www/drupal-7.26/modules/search/search-results.tpl.php b/kolab.org/www/drupal-7.26/modules/search/search-results.tpl.php
new file mode 100644
index 0000000..aa9bf8d
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search-results.tpl.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Default theme implementation for displaying search results.
+ *
+ * This template collects each invocation of theme_search_result(). This and
+ * the child template are dependent to one another sharing the markup for
+ * definition lists.
+ *
+ * Note that modules may implement their own search type and theme function
+ * completely bypassing this template.
+ *
+ * Available variables:
+ * - $search_results: All results as it is rendered through
+ * search-result.tpl.php
+ * - $module: The machine-readable name of the module (tab) being searched, such
+ * as "node" or "user".
+ *
+ *
+ * @see template_preprocess_search_results()
+ *
+ * @ingroup themeable
+ */
+?>
+<?php if ($search_results): ?>
+ <h2><?php print t('Search results');?></h2>
+ <ol class="search-results <?php print $module; ?>-results">
+ <?php print $search_results; ?>
+ </ol>
+ <?php print $pager; ?>
+<?php else : ?>
+ <h2><?php print t('Your search yielded no results');?></h2>
+ <?php print search_help('search#noresults', drupal_help_arg()); ?>
+<?php endif; ?>
diff --git a/kolab.org/www/drupal-7.26/modules/search/search-rtl.css b/kolab.org/www/drupal-7.26/modules/search/search-rtl.css
new file mode 100644
index 0000000..da9e8d9
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search-rtl.css
@@ -0,0 +1,13 @@
+
+.search-advanced .criterion {
+ float: right;
+ margin-right: 0;
+ margin-left: 2em;
+}
+.search-advanced .action {
+ float: right;
+ clear: right;
+}
+.search-results .search-snippet-info {
+ padding-right: 1em; /* LTR */
+} \ No newline at end of file
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.admin.inc b/kolab.org/www/drupal-7.26/modules/search/search.admin.inc
new file mode 100644
index 0000000..a609485
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.admin.inc
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * @file
+ * Admin page callbacks for the search module.
+ */
+
+/**
+ * Menu callback: confirm wiping of the index.
+ */
+function search_reindex_confirm() {
+ return confirm_form(array(), t('Are you sure you want to re-index the site?'),
+ 'admin/config/search/settings', t('The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel'));
+}
+
+/**
+ * Handler for wipe confirmation
+ */
+function search_reindex_confirm_submit(&$form, &$form_state) {
+ if ($form['confirm']) {
+ search_reindex();
+ drupal_set_message(t('The index will be rebuilt.'));
+ $form_state['redirect'] = 'admin/config/search/settings';
+ return;
+ }
+}
+
+/**
+ * Helper function to get real module names.
+ */
+function _search_get_module_names() {
+
+ $search_info = search_get_info(TRUE);
+ $system_info = system_get_info('module');
+ $names = array();
+ foreach ($search_info as $module => $info) {
+ $names[$module] = $system_info[$module]['name'];
+ }
+ asort($names, SORT_STRING);
+ return $names;
+}
+
+/**
+ * Menu callback: displays the search module settings page.
+ *
+ * @ingroup forms
+ *
+ * @see search_admin_settings_validate()
+ * @see search_admin_settings_submit()
+ * @see search_admin_reindex_submit()
+ */
+function search_admin_settings($form) {
+ // Collect some stats
+ $remaining = 0;
+ $total = 0;
+ foreach (variable_get('search_active_modules', array('node', 'user')) as $module) {
+ if ($status = module_invoke($module, 'search_status')) {
+ $remaining += $status['remaining'];
+ $total += $status['total'];
+ }
+ }
+
+ $count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
+ $percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) . '%';
+ $status = '<p><strong>' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
+ $form['status'] = array('#type' => 'fieldset', '#title' => t('Indexing status'));
+ $form['status']['status'] = array('#markup' => $status);
+ $form['status']['wipe'] = array('#type' => 'submit', '#value' => t('Re-index site'), '#submit' => array('search_admin_reindex_submit'));
+
+ $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500));
+
+ // Indexing throttle:
+ $form['indexing_throttle'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Indexing throttle')
+ );
+ $form['indexing_throttle']['search_cron_limit'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of items to index per cron run'),
+ '#default_value' => variable_get('search_cron_limit', 100),
+ '#options' => $items,
+ '#description' => t('The maximum number of items indexed in each pass of a <a href="@cron">cron maintenance task</a>. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status')))
+ );
+ // Indexing settings:
+ $form['indexing_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Indexing settings')
+ );
+ $form['indexing_settings']['info'] = array(
+ '#markup' => t('<p><em>Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.</em></p><p><em>The default settings should be appropriate for the majority of sites.</em></p>')
+ );
+ $form['indexing_settings']['minimum_word_size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Minimum word length to index'),
+ '#default_value' => variable_get('minimum_word_size', 3),
+ '#size' => 5,
+ '#maxlength' => 3,
+ '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).'),
+ '#element_validate' => array('element_validate_integer_positive'),
+ );
+ $form['indexing_settings']['overlap_cjk'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Simple CJK handling'),
+ '#default_value' => variable_get('overlap_cjk', TRUE),
+ '#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.')
+ );
+
+ $form['active'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Active search modules')
+ );
+ $module_options = _search_get_module_names();
+ $form['active']['search_active_modules'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Active modules'),
+ '#title_display' => 'invisible',
+ '#default_value' => variable_get('search_active_modules', array('node', 'user')),
+ '#options' => $module_options,
+ '#description' => t('Choose which search modules are active from the available modules.')
+ );
+ $form['active']['search_default_module'] = array(
+ '#title' => t('Default search module'),
+ '#type' => 'radios',
+ '#default_value' => variable_get('search_default_module', 'node'),
+ '#options' => $module_options,
+ '#description' => t('Choose which search module is the default.')
+ );
+ $form['#validate'][] = 'search_admin_settings_validate';
+ $form['#submit'][] = 'search_admin_settings_submit';
+
+ // Per module settings
+ foreach (variable_get('search_active_modules', array('node', 'user')) as $module) {
+ $added_form = module_invoke($module, 'search_admin');
+ if (is_array($added_form)) {
+ $form = array_merge($form, $added_form);
+ }
+ }
+
+ return system_settings_form($form);
+}
+
+/**
+ * Form validation handler for search_admin_settings().
+ */
+function search_admin_settings_validate($form, &$form_state) {
+ // Check whether we selected a valid default.
+ if ($form_state['triggering_element']['#value'] != t('Reset to defaults')) {
+ $new_modules = array_filter($form_state['values']['search_active_modules']);
+ $default = $form_state['values']['search_default_module'];
+ if (!in_array($default, $new_modules, TRUE)) {
+ form_set_error('search_default_module', t('Your default search module is not selected as an active module.'));
+ }
+ }
+}
+
+/**
+ * Form submission handler for search_admin_settings().
+ */
+function search_admin_settings_submit($form, &$form_state) {
+ // If these settings change, the index needs to be rebuilt.
+ if ((variable_get('minimum_word_size', 3) != $form_state['values']['minimum_word_size']) ||
+ (variable_get('overlap_cjk', TRUE) != $form_state['values']['overlap_cjk'])) {
+ drupal_set_message(t('The index will be rebuilt.'));
+ search_reindex();
+ }
+ $current_modules = variable_get('search_active_modules', array('node', 'user'));
+ // Check whether we are resetting the values.
+ if ($form_state['triggering_element']['#value'] == t('Reset to defaults')) {
+ $new_modules = array('node', 'user');
+ }
+ else {
+ $new_modules = array_filter($form_state['values']['search_active_modules']);
+ }
+ if (array_diff($current_modules, $new_modules)) {
+ drupal_set_message(t('The active search modules have been changed.'));
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+}
+
+/**
+ * Form submission handler for reindex button on search_admin_settings_form().
+ */
+function search_admin_reindex_submit($form, &$form_state) {
+ // send the user to the confirmation page
+ $form_state['redirect'] = 'admin/config/search/settings/reindex';
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.api.php b/kolab.org/www/drupal-7.26/modules/search/search.api.php
new file mode 100644
index 0000000..62d53b8
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.api.php
@@ -0,0 +1,376 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Search module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Define a custom search type.
+ *
+ * This hook allows a module to tell search.module that it wishes to perform
+ * searches on content it defines (custom node types, users, or comments for
+ * example) when a site search is performed.
+ *
+ * In order for the search to do anything, your module must also implement
+ * hook_search_execute(), which is called when someone requests a search
+ * on your module's type of content. If you want to have your content
+ * indexed in the standard search index, your module should also implement
+ * hook_update_index(). If your search type has settings, you can implement
+ * hook_search_admin() to add them to the search settings page. You can use
+ * hook_form_FORM_ID_alter(), with FORM_ID set to 'search_form', to add fields
+ * to the search form (see node_form_search_form_alter() for an example).
+ * You can use hook_search_access() to limit access to searching,
+ * and hook_search_page() to override how search results are displayed.
+ *
+ * @return
+ * Array with optional keys:
+ * - title: Title for the tab on the search page for this module. Defaults
+ * to the module name if not given.
+ * - path: Path component after 'search/' for searching with this module.
+ * Defaults to the module name if not given.
+ * - conditions_callback: An implementation of callback_search_conditions().
+ *
+ * @ingroup search
+ */
+function hook_search_info() {
+ return array(
+ 'title' => 'Content',
+ 'path' => 'node',
+ 'conditions_callback' => 'callback_search_conditions',
+ );
+}
+
+/**
+ * Define access to a custom search routine.
+ *
+ * This hook allows a module to define permissions for a search tab.
+ *
+ * @ingroup search
+ */
+function hook_search_access() {
+ return user_access('access content');
+}
+
+/**
+ * Take action when the search index is going to be rebuilt.
+ *
+ * Modules that use hook_update_index() should update their indexing
+ * bookkeeping so that it starts from scratch the next time
+ * hook_update_index() is called.
+ *
+ * @ingroup search
+ */
+function hook_search_reset() {
+ db_update('search_dataset')
+ ->fields(array('reindex' => REQUEST_TIME))
+ ->condition('type', 'node')
+ ->execute();
+}
+
+/**
+ * Report the status of indexing.
+ *
+ * The core search module only invokes this hook on active modules.
+ * Implementing modules do not need to check whether they are active when
+ * calculating their return values.
+ *
+ * @return
+ * An associative array with the key-value pairs:
+ * - 'remaining': The number of items left to index.
+ * - 'total': The total number of items to index.
+ *
+ * @ingroup search
+ */
+function hook_search_status() {
+ $total = db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')->fetchField();
+ $remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField();
+ return array('remaining' => $remaining, 'total' => $total);
+}
+
+/**
+ * Add elements to the search settings form.
+ *
+ * @return
+ * Form array for the Search settings page at admin/config/search/settings.
+ *
+ * @ingroup search
+ */
+function hook_search_admin() {
+ // Output form for defining rank factor weights.
+ $form['content_ranking'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Content ranking'),
+ );
+ $form['content_ranking']['#theme'] = 'node_search_admin';
+ $form['content_ranking']['info'] = array(
+ '#value' => '<em>' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
+ );
+
+ // Note: reversed to reflect that higher number = higher ranking.
+ $options = drupal_map_assoc(range(0, 10));
+ foreach (module_invoke_all('ranking') as $var => $values) {
+ $form['content_ranking']['factors']['node_rank_' . $var] = array(
+ '#title' => $values['title'],
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => variable_get('node_rank_' . $var, 0),
+ );
+ }
+ return $form;
+}
+
+/**
+ * Execute a search for a set of key words.
+ *
+ * Use database API with the 'PagerDefault' query extension to perform your
+ * search.
+ *
+ * If your module uses hook_update_index() and search_index() to index its
+ * items, use table 'search_index' aliased to 'i' as the main table in your
+ * query, with the 'SearchQuery' extension. You can join to your module's table
+ * using the 'i.sid' field, which will contain the $sid values you provided to
+ * search_index(). Add the main keywords to the query by using method
+ * searchExpression(). The functions search_expression_extract() and
+ * search_expression_insert() may also be helpful for adding custom search
+ * parameters to the search expression.
+ *
+ * See node_search_execute() for an example of a module that uses the search
+ * index, and user_search_execute() for an example that doesn't use the search
+ * index.
+ *
+ * @param $keys
+ * The search keywords as entered by the user.
+ * @param $conditions
+ * An optional array of additional conditions, such as filters.
+ *
+ * @return
+ * An array of search results. To use the default search result
+ * display, each item should have the following keys':
+ * - 'link': Required. The URL of the found item.
+ * - 'type': The type of item (such as the content type).
+ * - 'title': Required. The name of the item.
+ * - 'user': The author of the item.
+ * - 'date': A timestamp when the item was last modified.
+ * - 'extra': An array of optional extra information items.
+ * - 'snippet': An excerpt or preview to show with the result (can be
+ * generated with search_excerpt()).
+ * - 'language': Language code for the item (usually two characters).
+ *
+ * @ingroup search
+ */
+function hook_search_execute($keys = NULL, $conditions = NULL) {
+ // Build matching conditions
+ $query = db_select('search_index', 'i', array('target' => 'slave'))->extend('SearchQuery')->extend('PagerDefault');
+ $query->join('node', 'n', 'n.nid = i.sid');
+ $query
+ ->condition('n.status', 1)
+ ->addTag('node_access')
+ ->searchExpression($keys, 'node');
+
+ // Insert special keywords.
+ $query->setOption('type', 'n.type');
+ $query->setOption('language', 'n.language');
+ if ($query->setOption('term', 'ti.tid')) {
+ $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
+ }
+ // Only continue if the first pass query matches.
+ if (!$query->executeFirstPass()) {
+ return array();
+ }
+
+ // Add the ranking expressions.
+ _node_rankings($query);
+
+ // Load results.
+ $find = $query
+ ->limit(10)
+ ->execute();
+ $results = array();
+ foreach ($find as $item) {
+ // Build the node body.
+ $node = node_load($item->sid);
+ node_build_content($node, 'search_result');
+ $node->body = drupal_render($node->content);
+
+ // Fetch comments for snippet.
+ $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node);
+ // Fetch terms for snippet.
+ $node->rendered .= ' ' . module_invoke('taxonomy', 'node_update_index', $node);
+
+ $extra = module_invoke_all('node_search_result', $node);
+
+ $results[] = array(
+ 'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
+ 'type' => check_plain(node_type_get_name($node)),
+ 'title' => $node->title,
+ 'user' => theme('username', array('account' => $node)),
+ 'date' => $node->changed,
+ 'node' => $node,
+ 'extra' => $extra,
+ 'score' => $item->calculated_score,
+ 'snippet' => search_excerpt($keys, $node->body),
+ );
+ }
+ return $results;
+}
+
+/**
+ * Override the rendering of search results.
+ *
+ * A module that implements hook_search_info() to define a type of search may
+ * implement this hook in order to override the default theming of its search
+ * results, which is otherwise themed using theme('search_results').
+ *
+ * Note that by default, theme('search_results') and theme('search_result')
+ * work together to create an ordered list (OL). So your hook_search_page()
+ * implementation should probably do this as well.
+ *
+ * @param $results
+ * An array of search results.
+ *
+ * @return
+ * A renderable array, which will render the formatted search results with a
+ * pager included.
+ *
+ * @see search-result.tpl.php
+ * @see search-results.tpl.php
+ */
+function hook_search_page($results) {
+ $output['prefix']['#markup'] = '<ol class="search-results">';
+
+ foreach ($results as $entry) {
+ $output[] = array(
+ '#theme' => 'search_result',
+ '#result' => $entry,
+ '#module' => 'my_module_name',
+ );
+ }
+ $output['suffix']['#markup'] = '</ol>' . theme('pager');
+
+ return $output;
+}
+
+/**
+ * Preprocess text for search.
+ *
+ * This hook is called to preprocess both the text added to the search index and
+ * the keywords users have submitted for searching.
+ *
+ * Possible uses:
+ * - Adding spaces between words of Chinese or Japanese text.
+ * - Stemming words down to their root words to allow matches between, for
+ * instance, walk, walked, walking, and walks in searching.
+ * - Expanding abbreviations and acronymns that occur in text.
+ *
+ * @param $text
+ * The text to preprocess. This is a single piece of plain text extracted
+ * from between two HTML tags or from the search query. It will not contain
+ * any HTML entities or HTML tags.
+ *
+ * @return
+ * The text after preprocessing. Note that if your module decides not to alter
+ * the text, it should return the original text. Also, after preprocessing,
+ * words in the text should be separated by a space.
+ *
+ * @ingroup search
+ */
+function hook_search_preprocess($text) {
+ // Do processing on $text
+ return $text;
+}
+
+/**
+ * Update the search index for this module.
+ *
+ * This hook is called every cron run if search.module is enabled, your
+ * module has implemented hook_search_info(), and your module has been set as
+ * an active search module on the Search settings page
+ * (admin/config/search/settings). It allows your module to add items to the
+ * built-in search index using search_index(), or to add them to your module's
+ * own indexing mechanism.
+ *
+ * When implementing this hook, your module should index content items that
+ * were modified or added since the last run. PHP has a time limit
+ * for cron, though, so it is advisable to limit how many items you index
+ * per run using variable_get('search_cron_limit') (see example below). Also,
+ * since the cron run could time out and abort in the middle of your run, you
+ * should update your module's internal bookkeeping on when items have last
+ * been indexed as you go rather than waiting to the end of indexing.
+ *
+ * @ingroup search
+ */
+function hook_update_index() {
+ $limit = (int)variable_get('search_cron_limit', 100);
+
+ $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit);
+
+ foreach ($result as $node) {
+ $node = node_load($node->nid);
+
+ // Save the changed time of the most recent indexed node, for the search
+ // results half-life calculation.
+ variable_set('node_cron_last', $node->changed);
+
+ // Render the node.
+ node_build_content($node, 'search_index');
+ $node->rendered = drupal_render($node->content);
+
+ $text = '<h1>' . check_plain($node->title) . '</h1>' . $node->rendered;
+
+ // Fetch extra data normally not visible
+ $extra = module_invoke_all('node_update_index', $node);
+ foreach ($extra as $t) {
+ $text .= $t;
+ }
+
+ // Update index
+ search_index($node->nid, 'node', $text);
+ }
+}
+/**
+ * @} End of "addtogroup hooks".
+ */
+
+/**
+ * Provide search query conditions.
+ *
+ * Callback for hook_search_info().
+ *
+ * This callback is invoked by search_view() to get an array of additional
+ * search conditions to pass to search_data(). For example, a search module
+ * may get additional keywords, filters, or modifiers for the search from
+ * the query string.
+ *
+ * This example pulls additional search keywords out of the $_REQUEST variable,
+ * (i.e. from the query string of the request). The conditions may also be
+ * generated internally - for example based on a module's settings.
+ *
+ * @param $keys
+ * The search keywords string.
+ *
+ * @return
+ * An array of additional conditions, such as filters.
+ *
+ * @ingroup callbacks
+ * @ingroup search
+ */
+function callback_search_conditions($keys) {
+ $conditions = array();
+
+ if (!empty($_REQUEST['keys'])) {
+ $conditions['keys'] = $_REQUEST['keys'];
+ }
+ if (!empty($_REQUEST['sample_search_keys'])) {
+ $conditions['sample_search_keys'] = $_REQUEST['sample_search_keys'];
+ }
+ if ($force_keys = config('sample_search.settings')->get('force_keywords')) {
+ $conditions['sample_search_force_keywords'] = $force_keys;
+ }
+ return $conditions;
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.css b/kolab.org/www/drupal-7.26/modules/search/search.css
new file mode 100644
index 0000000..ff7230f
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.css
@@ -0,0 +1,34 @@
+
+.search-form {
+ margin-bottom: 1em;
+}
+.search-form input {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+.search-results {
+ list-style: none;
+}
+.search-results p {
+ margin-top: 0;
+}
+.search-results .title {
+ font-size: 1.2em;
+}
+.search-results li {
+ margin-bottom: 1em;
+}
+.search-results .search-snippet-info {
+ padding-left: 1em; /* LTR */
+}
+.search-results .search-info {
+ font-size: 0.85em;
+}
+.search-advanced .criterion {
+ float: left; /* LTR */
+ margin-right: 2em; /* LTR */
+}
+.search-advanced .action {
+ float: left; /* LTR */
+ clear: left; /* LTR */
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.extender.inc b/kolab.org/www/drupal-7.26/modules/search/search.extender.inc
new file mode 100644
index 0000000..6709466
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.extender.inc
@@ -0,0 +1,536 @@
+<?php
+
+/**
+ * @file
+ * Search query extender and helper functions.
+ */
+
+/**
+ * Do a query on the full-text search index for a word or words.
+ *
+ * This function is normally only called by each module that supports the
+ * indexed search (and thus, implements hook_update_index()).
+ *
+ * Results are retrieved in two logical passes. However, the two passes are
+ * joined together into a single query. And in the case of most simple
+ * queries the second pass is not even used.
+ *
+ * The first pass selects a set of all possible matches, which has the benefit
+ * of also providing the exact result set for simple "AND" or "OR" searches.
+ *
+ * The second portion of the query further refines this set by verifying
+ * advanced text conditions (such as negative or phrase matches).
+ *
+ * The used query object has the tag 'search_$module' and can be further
+ * extended with hook_query_alter().
+ */
+class SearchQuery extends SelectQueryExtender {
+ /**
+ * The search query that is used for searching.
+ *
+ * @var string
+ */
+ protected $searchExpression;
+
+ /**
+ * Type of search (search module).
+ *
+ * This maps to the value of the type column in search_index, and is equal
+ * to the machine-readable name of the module that implements
+ * hook_search_info().
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * Positive and negative search keys.
+ *
+ * @var array
+ */
+ protected $keys = array('positive' => array(), 'negative' => array());
+
+ /**
+ * Indicates whether the first pass query requires complex conditions (LIKE).
+ *
+ * @var boolean.
+ */
+ protected $simple = TRUE;
+
+ /**
+ * Conditions that are used for exact searches.
+ *
+ * This is always used for the second pass query but not for the first pass,
+ * unless $this->simple is FALSE.
+ *
+ * @var DatabaseCondition
+ */
+ protected $conditions;
+
+ /**
+ * Indicates how many matches for a search query are necessary.
+ *
+ * @var int
+ */
+ protected $matches = 0;
+
+ /**
+ * Array of search words.
+ *
+ * These words have to match against {search_index}.word.
+ *
+ * @var array
+ */
+ protected $words = array();
+
+ /**
+ * Multiplier for the normalized search score.
+ *
+ * This value is calculated by the first pass query and multiplied with the
+ * actual score of a specific word to make sure that the resulting calculated
+ * score is between 0 and 1.
+ *
+ * @var float
+ */
+ protected $normalize;
+
+ /**
+ * Indicates whether the first pass query has been executed.
+ *
+ * @var boolean
+ */
+ protected $executedFirstPass = FALSE;
+
+ /**
+ * Stores score expressions.
+ *
+ * @var array
+ *
+ * @see addScore()
+ */
+ protected $scores = array();
+
+ /**
+ * Stores arguments for score expressions.
+ *
+ * @var array
+ */
+ protected $scoresArguments = array();
+
+ /**
+ * Stores multipliers for score expressions.
+ *
+ * @var array
+ */
+ protected $multiply = array();
+
+ /**
+ * Whether or not search expressions were ignored.
+ *
+ * The maximum number of AND/OR combinations exceeded can be configured to
+ * avoid Denial-of-Service attacks. Expressions beyond the limit are ignored.
+ *
+ * @var boolean
+ */
+ protected $expressionsIgnored = FALSE;
+
+ /**
+ * Sets up the search query expression.
+ *
+ * @param $query
+ * A search query string, which can contain options.
+ * @param $module
+ * The search module. This maps to {search_index}.type in the database.
+ *
+ * @return
+ * The SearchQuery object.
+ */
+ public function searchExpression($expression, $module) {
+ $this->searchExpression = $expression;
+ $this->type = $module;
+
+ return $this;
+ }
+
+ /**
+ * Applies a search option and removes it from the search query string.
+ *
+ * These options are in the form option:value,value2,value3.
+ *
+ * @param $option
+ * Name of the option.
+ * @param $column
+ * Name of the database column to which the value should be applied.
+ *
+ * @return
+ * TRUE if a value for that option was found, FALSE if not.
+ */
+ public function setOption($option, $column) {
+ if ($values = search_expression_extract($this->searchExpression, $option)) {
+ $or = db_or();
+ foreach (explode(',', $values) as $value) {
+ $or->condition($column, $value);
+ }
+ $this->condition($or);
+ $this->searchExpression = search_expression_insert($this->searchExpression, $option);
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Parses the search query into SQL conditions.
+ *
+ * We build two queries that match the dataset bodies.
+ */
+ protected function parseSearchExpression() {
+ // Matchs words optionally prefixed by a dash. A word in this case is
+ // something between two spaces, optionally quoted.
+ preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' ' . $this->searchExpression , $keywords, PREG_SET_ORDER);
+
+ if (count($keywords) == 0) {
+ return;
+ }
+
+ // Classify tokens.
+ $or = FALSE;
+ $warning = '';
+ $limit_combinations = variable_get('search_and_or_limit', 7);
+ // The first search expression does not count as AND.
+ $and_count = -1;
+ $or_count = 0;
+ foreach ($keywords as $match) {
+ if ($or_count && $and_count + $or_count >= $limit_combinations) {
+ // Ignore all further search expressions to prevent Denial-of-Service
+ // attacks using a high number of AND/OR combinations.
+ $this->expressionsIgnored = TRUE;
+ break;
+ }
+ $phrase = FALSE;
+ // Strip off phrase quotes.
+ if ($match[2]{0} == '"') {
+ $match[2] = substr($match[2], 1, -1);
+ $phrase = TRUE;
+ $this->simple = FALSE;
+ }
+ // Simplify keyword according to indexing rules and external
+ // preprocessors. Use same process as during search indexing, so it
+ // will match search index.
+ $words = search_simplify($match[2]);
+ // Re-explode in case simplification added more words, except when
+ // matching a phrase.
+ $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
+ // Negative matches.
+ if ($match[1] == '-') {
+ $this->keys['negative'] = array_merge($this->keys['negative'], $words);
+ }
+ // OR operator: instead of a single keyword, we store an array of all
+ // OR'd keywords.
+ elseif ($match[2] == 'OR' && count($this->keys['positive'])) {
+ $last = array_pop($this->keys['positive']);
+ // Starting a new OR?
+ if (!is_array($last)) {
+ $last = array($last);
+ }
+ $this->keys['positive'][] = $last;
+ $or = TRUE;
+ $or_count++;
+ continue;
+ }
+ // AND operator: implied, so just ignore it.
+ elseif ($match[2] == 'AND' || $match[2] == 'and') {
+ $warning = $match[2];
+ continue;
+ }
+
+ // Plain keyword.
+ else {
+ if ($match[2] == 'or') {
+ $warning = $match[2];
+ }
+ if ($or) {
+ // Add to last element (which is an array).
+ $this->keys['positive'][count($this->keys['positive']) - 1] = array_merge($this->keys['positive'][count($this->keys['positive']) - 1], $words);
+ }
+ else {
+ $this->keys['positive'] = array_merge($this->keys['positive'], $words);
+ $and_count++;
+ }
+ }
+ $or = FALSE;
+ }
+
+ // Convert keywords into SQL statements.
+ $this->conditions = db_and();
+ $simple_and = FALSE;
+ $simple_or = FALSE;
+ // Positive matches.
+ foreach ($this->keys['positive'] as $key) {
+ // Group of ORed terms.
+ if (is_array($key) && count($key)) {
+ $simple_or = TRUE;
+ $any = FALSE;
+ $queryor = db_or();
+ foreach ($key as $or) {
+ list($num_new_scores) = $this->parseWord($or);
+ $any |= $num_new_scores;
+ $queryor->condition('d.data', "% $or %", 'LIKE');
+ }
+ if (count($queryor)) {
+ $this->conditions->condition($queryor);
+ // A group of OR keywords only needs to match once.
+ $this->matches += ($any > 0);
+ }
+ }
+ // Single ANDed term.
+ else {
+ $simple_and = TRUE;
+ list($num_new_scores, $num_valid_words) = $this->parseWord($key);
+ $this->conditions->condition('d.data', "% $key %", 'LIKE');
+ if (!$num_valid_words) {
+ $this->simple = FALSE;
+ }
+ // Each AND keyword needs to match at least once.
+ $this->matches += $num_new_scores;
+ }
+ }
+ if ($simple_and && $simple_or) {
+ $this->simple = FALSE;
+ }
+ // Negative matches.
+ foreach ($this->keys['negative'] as $key) {
+ $this->conditions->condition('d.data', "% $key %", 'NOT LIKE');
+ $this->simple = FALSE;
+ }
+
+ if ($warning == 'or') {
+ drupal_set_message(t('Search for either of the two terms with uppercase <strong>OR</strong>. For example, <strong>cats OR dogs</strong>.'));
+ }
+ }
+
+ /**
+ * Helper function for parseQuery().
+ */
+ protected function parseWord($word) {
+ $num_new_scores = 0;
+ $num_valid_words = 0;
+ // Determine the scorewords of this word/phrase.
+ $split = explode(' ', $word);
+ foreach ($split as $s) {
+ $num = is_numeric($s);
+ if ($num || drupal_strlen($s) >= variable_get('minimum_word_size', 3)) {
+ if (!isset($this->words[$s])) {
+ $this->words[$s] = $s;
+ $num_new_scores++;
+ }
+ $num_valid_words++;
+ }
+ }
+ // Return matching snippet and number of added words.
+ return array($num_new_scores, $num_valid_words);
+ }
+
+ /**
+ * Executes the first pass query.
+ *
+ * This can either be done explicitly, so that additional scores and
+ * conditions can be applied to the second pass query, or implicitly by
+ * addScore() or execute().
+ *
+ * @return
+ * TRUE if search items exist, FALSE if not.
+ */
+ public function executeFirstPass() {
+ $this->parseSearchExpression();
+
+ if (count($this->words) == 0) {
+ form_set_error('keys', format_plural(variable_get('minimum_word_size', 3), 'You must include at least one positive keyword with 1 character or more.', 'You must include at least one positive keyword with @count characters or more.'));
+ return FALSE;
+ }
+ if ($this->expressionsIgnored) {
+ drupal_set_message(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => variable_get('search_and_or_limit', 7))), 'warning');
+ }
+ $this->executedFirstPass = TRUE;
+
+ if (!empty($this->words)) {
+ $or = db_or();
+ foreach ($this->words as $word) {
+ $or->condition('i.word', $word);
+ }
+ $this->condition($or);
+ }
+ // Build query for keyword normalization.
+ $this->join('search_total', 't', 'i.word = t.word');
+ $this
+ ->condition('i.type', $this->type)
+ ->groupBy('i.type')
+ ->groupBy('i.sid')
+ ->having('COUNT(*) >= :matches', array(':matches' => $this->matches));
+
+ // Clone the query object to do the firstPass query;
+ $first = clone $this->query;
+
+ // For complex search queries, add the LIKE conditions to the first pass query.
+ if (!$this->simple) {
+ $first->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
+ $first->condition($this->conditions);
+ }
+
+ // Calculate maximum keyword relevance, to normalize it.
+ $first->addExpression('SUM(i.score * t.count)', 'calculated_score');
+ $this->normalize = $first
+ ->range(0, 1)
+ ->orderBy('calculated_score', 'DESC')
+ ->execute()
+ ->fetchField();
+
+ if ($this->normalize) {
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Adds a custom score expression to the search query.
+ *
+ * Score expressions are used to order search results. If no calls to
+ * addScore() have taken place, a default keyword relevance score will be
+ * used. However, if at least one call to addScore() has taken place, the
+ * keyword relevance score is not automatically added.
+ *
+ * Also note that if you call orderBy() directly on the query, search scores
+ * will not automatically be used to order search results. Your orderBy()
+ * expression can reference 'calculated_score', which will be the total
+ * calculated score value.
+ *
+ * @param $score
+ * The score expression, which should evaluate to a number between 0 and 1.
+ * The string 'i.relevance' in a score expression will be replaced by a
+ * measure of keyword relevance between 0 and 1.
+ * @param $arguments
+ * Query arguments needed to provide values to the score expression.
+ * @param $multiply
+ * If set, the score is multiplied with this value. However, all scores
+ * with multipliers are then divided by the total of all multipliers, so
+ * that overall, the normalization is maintained.
+ *
+ * @return object
+ * The updated query object.
+ */
+ public function addScore($score, $arguments = array(), $multiply = FALSE) {
+ if ($multiply) {
+ $i = count($this->multiply);
+ // Modify the score expression so it is multiplied by the multiplier,
+ // with a divisor to renormalize.
+ $score = "CAST(:multiply_$i AS DECIMAL) * COALESCE(( " . $score . "), 0) / CAST(:total_$i AS DECIMAL)";
+ // Add an argument for the multiplier. The :total_$i argument is taken
+ // care of in the execute() method, which is when the total divisor is
+ // calculated.
+ $arguments[':multiply_' . $i] = $multiply;
+ $this->multiply[] = $multiply;
+ }
+
+ $this->scores[] = $score;
+ $this->scoresArguments += $arguments;
+
+ return $this;
+ }
+
+ /**
+ * Executes the search.
+ *
+ * If not already done, this executes the first pass query. Then the complex
+ * conditions are applied to the query including score expressions and
+ * ordering.
+ *
+ * @return
+ * FALSE if the first pass query returned no results, and a database result
+ * set if there were results.
+ */
+ public function execute()
+ {
+ if (!$this->executedFirstPass) {
+ $this->executeFirstPass();
+ }
+ if (!$this->normalize) {
+ return new DatabaseStatementEmpty();
+ }
+
+ // Add conditions to query.
+ $this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
+ $this->condition($this->conditions);
+
+ if (empty($this->scores)) {
+ // Add default score.
+ $this->addScore('i.relevance');
+ }
+
+ if (count($this->multiply)) {
+ // Re-normalize scores with multipliers by dividing by the total of all
+ // multipliers. The expressions were altered in addScore(), so here just
+ // add the arguments for the total.
+ $i = 0;
+ $sum = array_sum($this->multiply);
+ foreach ($this->multiply as $total) {
+ $this->scoresArguments[':total_' . $i] = $sum;
+ $i++;
+ }
+ }
+
+ // Replace the pseudo-expression 'i.relevance' with a measure of keyword
+ // relevance in all score expressions, using string replacement. Careful
+ // though! If you just print out a float, some locales use ',' as the
+ // decimal separator in PHP, while SQL always uses '.'. So, make sure to
+ // set the number format correctly.
+ $relevance = number_format((1.0 / $this->normalize), 10, '.', '');
+ $this->scores = str_replace('i.relevance', '(' . $relevance . ' * i.score * t.count)', $this->scores);
+
+ // Add all scores together to form a query field.
+ $this->addExpression('SUM(' . implode(' + ', $this->scores) . ')', 'calculated_score', $this->scoresArguments);
+
+ // If an order has not yet been set for this query, add a default order
+ // that sorts by the calculated sum of scores.
+ if (count($this->getOrderBy()) == 0) {
+ $this->orderBy('calculated_score', 'DESC');
+ }
+
+ // Add tag and useful metadata.
+ $this
+ ->addTag('search_' . $this->type)
+ ->addMetaData('normalize', $this->normalize)
+ ->fields('i', array('type', 'sid'));
+
+ return $this->query->execute();
+ }
+
+ /**
+ * Builds the default count query for SearchQuery.
+ *
+ * Since SearchQuery always uses GROUP BY, we can default to a subquery. We
+ * also add the same conditions as execute() because countQuery() is called
+ * first.
+ */
+ public function countQuery() {
+ // Clone the inner query.
+ $inner = clone $this->query;
+
+ // Add conditions to query.
+ $inner->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
+ $inner->condition($this->conditions);
+
+ // Remove existing fields and expressions, they are not needed for a count
+ // query.
+ $fields =& $inner->getFields();
+ $fields = array();
+ $expressions =& $inner->getExpressions();
+ $expressions = array();
+
+ // Add the sid as the only field and count them as a subquery.
+ $count = db_select($inner->fields('i', array('sid')), NULL, array('target' => 'slave'));
+
+ // Add the COUNT() expression.
+ $count->addExpression('COUNT(*)');
+
+ return $count;
+ }
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.info b/kolab.org/www/drupal-7.26/modules/search/search.info
new file mode 100644
index 0000000..3c0495d
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.info
@@ -0,0 +1,15 @@
+name = Search
+description = Enables site-wide keyword searching.
+package = Core
+version = VERSION
+core = 7.x
+files[] = search.extender.inc
+files[] = search.test
+configure = admin/config/search/settings
+stylesheets[all][] = search.css
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.install b/kolab.org/www/drupal-7.26/modules/search/search.install
new file mode 100644
index 0000000..f0113b3
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.install
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the search module.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function search_uninstall() {
+ variable_del('minimum_word_size');
+ variable_del('overlap_cjk');
+ variable_del('search_cron_limit');
+}
+
+/**
+ * Implements hook_schema().
+ */
+function search_schema() {
+ $schema['search_dataset'] = array(
+ 'description' => 'Stores items that will be searched.',
+ 'fields' => array(
+ 'sid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Search item ID, e.g. node ID for nodes.',
+ ),
+ 'type' => array(
+ 'type' => 'varchar',
+ 'length' => 16,
+ 'not null' => TRUE,
+ 'description' => 'Type of item, e.g. node.',
+ ),
+ 'data' => array(
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'description' => 'List of space-separated words from the item.',
+ ),
+ 'reindex' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Set to force node reindexing.',
+ ),
+ ),
+ 'primary key' => array('sid', 'type'),
+ );
+
+ $schema['search_index'] = array(
+ 'description' => 'Stores the search index, associating words, items and scores.',
+ 'fields' => array(
+ 'word' => array(
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The {search_total}.word that is associated with the search item.',
+ ),
+ 'sid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {search_dataset}.sid of the searchable item to which the word belongs.',
+ ),
+ 'type' => array(
+ 'type' => 'varchar',
+ 'length' => 16,
+ 'not null' => TRUE,
+ 'description' => 'The {search_dataset}.type of the searchable item to which the word belongs.',
+ ),
+ 'score' => array(
+ 'type' => 'float',
+ 'not null' => FALSE,
+ 'description' => 'The numeric score of the word, higher being more important.',
+ ),
+ ),
+ 'indexes' => array(
+ 'sid_type' => array('sid', 'type'),
+ ),
+ 'foreign keys' => array(
+ 'search_dataset' => array(
+ 'table' => 'search_dataset',
+ 'columns' => array(
+ 'sid' => 'sid',
+ 'type' => 'type',
+ ),
+ ),
+ ),
+ 'primary key' => array('word', 'sid', 'type'),
+ );
+
+ $schema['search_total'] = array(
+ 'description' => 'Stores search totals for words.',
+ 'fields' => array(
+ 'word' => array(
+ 'description' => 'Primary Key: Unique word in the search index.',
+ 'type' => 'varchar',
+ 'length' => 50,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'count' => array(
+ 'description' => "The count of the word in the index using Zipf's law to equalize the probability distribution.",
+ 'type' => 'float',
+ 'not null' => FALSE,
+ ),
+ ),
+ 'primary key' => array('word'),
+ );
+
+ $schema['search_node_links'] = array(
+ 'description' => 'Stores items (like nodes) that link to other nodes, used to improve search scores for nodes that are frequently linked to.',
+ 'fields' => array(
+ 'sid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {search_dataset}.sid of the searchable item containing the link to the node.',
+ ),
+ 'type' => array(
+ 'type' => 'varchar',
+ 'length' => 16,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The {search_dataset}.type of the searchable item containing the link to the node.',
+ ),
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {node}.nid that this item links to.',
+ ),
+ 'caption' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'description' => 'The text used to link to the {node}.nid.',
+ ),
+ ),
+ 'primary key' => array('sid', 'type', 'nid'),
+ 'indexes' => array(
+ 'nid' => array('nid'),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Replace unique keys in 'search_dataset' and 'search_index' by primary keys.
+ */
+function search_update_7000() {
+ db_drop_unique_key('search_dataset', 'sid_type');
+ $dataset_type_spec = array(
+ 'type' => 'varchar',
+ 'length' => 16,
+ 'not null' => TRUE,
+ 'description' => 'Type of item, e.g. node.',
+ );
+ db_change_field('search_dataset', 'type', 'type', $dataset_type_spec);
+ db_add_primary_key('search_dataset', array('sid', 'type'));
+
+ db_drop_index('search_index', 'word');
+ db_drop_unique_key('search_index', 'word_sid_type');
+ $index_type_spec = array(
+ 'type' => 'varchar',
+ 'length' => 16,
+ 'not null' => TRUE,
+ 'description' => 'The {search_dataset}.type of the searchable item to which the word belongs.',
+ );
+ db_change_field('search_index', 'type', 'type', $index_type_spec);
+ db_add_primary_key('search_index', array('word', 'sid', 'type'));
+}
+
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.module b/kolab.org/www/drupal-7.26/modules/search/search.module
new file mode 100644
index 0000000..7542f98
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.module
@@ -0,0 +1,1356 @@
+<?php
+
+/**
+ * @file
+ * Enables site-wide keyword searching.
+ */
+
+/**
+ * Matches all 'N' Unicode character classes (numbers)
+ */
+define('PREG_CLASS_NUMBERS',
+ '\x{30}-\x{39}\x{b2}\x{b3}\x{b9}\x{bc}-\x{be}\x{660}-\x{669}\x{6f0}-\x{6f9}' .
+ '\x{966}-\x{96f}\x{9e6}-\x{9ef}\x{9f4}-\x{9f9}\x{a66}-\x{a6f}\x{ae6}-\x{aef}' .
+ '\x{b66}-\x{b6f}\x{be7}-\x{bf2}\x{c66}-\x{c6f}\x{ce6}-\x{cef}\x{d66}-\x{d6f}' .
+ '\x{e50}-\x{e59}\x{ed0}-\x{ed9}\x{f20}-\x{f33}\x{1040}-\x{1049}\x{1369}-' .
+ '\x{137c}\x{16ee}-\x{16f0}\x{17e0}-\x{17e9}\x{17f0}-\x{17f9}\x{1810}-\x{1819}' .
+ '\x{1946}-\x{194f}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2153}-\x{2183}' .
+ '\x{2460}-\x{249b}\x{24ea}-\x{24ff}\x{2776}-\x{2793}\x{3007}\x{3021}-\x{3029}' .
+ '\x{3038}-\x{303a}\x{3192}-\x{3195}\x{3220}-\x{3229}\x{3251}-\x{325f}\x{3280}-' .
+ '\x{3289}\x{32b1}-\x{32bf}\x{ff10}-\x{ff19}');
+
+/**
+ * Matches all 'P' Unicode character classes (punctuation)
+ */
+define('PREG_CLASS_PUNCTUATION',
+ '\x{21}-\x{23}\x{25}-\x{2a}\x{2c}-\x{2f}\x{3a}\x{3b}\x{3f}\x{40}\x{5b}-\x{5d}' .
+ '\x{5f}\x{7b}\x{7d}\x{a1}\x{ab}\x{b7}\x{bb}\x{bf}\x{37e}\x{387}\x{55a}-\x{55f}' .
+ '\x{589}\x{58a}\x{5be}\x{5c0}\x{5c3}\x{5f3}\x{5f4}\x{60c}\x{60d}\x{61b}\x{61f}' .
+ '\x{66a}-\x{66d}\x{6d4}\x{700}-\x{70d}\x{964}\x{965}\x{970}\x{df4}\x{e4f}' .
+ '\x{e5a}\x{e5b}\x{f04}-\x{f12}\x{f3a}-\x{f3d}\x{f85}\x{104a}-\x{104f}\x{10fb}' .
+ '\x{1361}-\x{1368}\x{166d}\x{166e}\x{169b}\x{169c}\x{16eb}-\x{16ed}\x{1735}' .
+ '\x{1736}\x{17d4}-\x{17d6}\x{17d8}-\x{17da}\x{1800}-\x{180a}\x{1944}\x{1945}' .
+ '\x{2010}-\x{2027}\x{2030}-\x{2043}\x{2045}-\x{2051}\x{2053}\x{2054}\x{2057}' .
+ '\x{207d}\x{207e}\x{208d}\x{208e}\x{2329}\x{232a}\x{23b4}-\x{23b6}\x{2768}-' .
+ '\x{2775}\x{27e6}-\x{27eb}\x{2983}-\x{2998}\x{29d8}-\x{29db}\x{29fc}\x{29fd}' .
+ '\x{3001}-\x{3003}\x{3008}-\x{3011}\x{3014}-\x{301f}\x{3030}\x{303d}\x{30a0}' .
+ '\x{30fb}\x{fd3e}\x{fd3f}\x{fe30}-\x{fe52}\x{fe54}-\x{fe61}\x{fe63}\x{fe68}' .
+ '\x{fe6a}\x{fe6b}\x{ff01}-\x{ff03}\x{ff05}-\x{ff0a}\x{ff0c}-\x{ff0f}\x{ff1a}' .
+ '\x{ff1b}\x{ff1f}\x{ff20}\x{ff3b}-\x{ff3d}\x{ff3f}\x{ff5b}\x{ff5d}\x{ff5f}-' .
+ '\x{ff65}');
+
+/**
+ * Matches CJK (Chinese, Japanese, Korean) letter-like characters.
+ *
+ * This list is derived from the "East Asian Scripts" section of
+ * http://www.unicode.org/charts/index.html, as well as a comment on
+ * http://unicode.org/reports/tr11/tr11-11.html listing some character
+ * ranges that are reserved for additional CJK ideographs.
+ *
+ * The character ranges do not include numbers, punctuation, or symbols, since
+ * these are handled separately in search. Note that radicals and strokes are
+ * considered symbols. (See
+ * http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt)
+ *
+ * @see search_expand_cjk()
+ */
+define('PREG_CLASS_CJK', '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' .
+ '\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' .
+ '\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' .
+ '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
+ '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}');
+
+/**
+ * Implements hook_help().
+ */
+function search_help($path, $arg) {
+ switch ($path) {
+ case 'admin/help#search':
+ $output = '';
+ $output .= '<h3>' . t('About') . '</h3>';
+ $output .= '<p>' . t('The Search module provides the ability to index and search for content by exact keywords, and for users by username or e-mail. For more information, see the online handbook entry for <a href="@search-module">Search module</a>.', array('@search-module' => 'http://drupal.org/documentation/modules/search/', '@search' => url('search'))) . '</p>';
+ $output .= '<h3>' . t('Uses') . '</h3>';
+ $output .= '<dl>';
+ $output .= '<dt>' . t('Searching content and users') . '</dt>';
+ $output .= '<dd>' . t('Users with <em>Use search</em> permission can use the search block and <a href="@search">Search page</a>. Users with the <em>View published content</em> permission can search for content containing exact keywords. Users with the <em>View user profiles</em> permission can search for users containing the keyword anywhere in the user name, and users with the <em>Administer users</em> permission can search for users by email address. Additionally, users with <em>Use advanced search</em> permission can find content using more complex search methods and filtering by choosing the <em>Advanced search</em> option on the <a href="@search">Search page</a>.', array('@search' => url('search'))) . '</dd>';
+ $output .= '<dt>' . t('Indexing content with cron') . '</dt>';
+ $output .= '<dd>' . t('To provide keyword searching, the search engine maintains an index of words found in the content and its fields, along with text added to your content by other modules (such as comments from the core Comment module, and taxonomy terms from the core Taxonomy module). To build and maintain this index, a correctly configured <a href="@cron">cron maintenance task</a> is required. Users with <em>Administer search</em> permission can further configure the cron settings on the <a href="@searchsettings">Search settings page</a>.', array('@cron' => 'http://drupal.org/cron', '@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
+ $output .= '<dt>' . t('Content reindexing') . '</dt>';
+ $output .= '<dd>' . t('Content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. Unlike content-related actions, actions related to the structure of your site do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, enabling or disabling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the <a href="@searchsettings">Search settings page</a>. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', array('@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
+ $output .= '<dt>' . t('Configuring search settings') . '</dt>';
+ $output .= '<dd>' . t('Indexing behavior can be adjusted using the <a href="@searchsettings">Search settings page</a>. Users with <em>Administer search</em> permission can control settings such as the <em>Number of items to index per cron run</em>, <em>Indexing settings</em> (word length), <em>Active search modules</em>, and <em>Content ranking</em>, which lets you adjust the priority in which indexed content is returned in results.', array('@searchsettings' => url('admin/config/search/settings'))) . '</dd>';
+ $output .= '<dt>' . t('Search block') . '</dt>';
+ $output .= '<dd>' . t('The Search module includes a default <em>Search form</em> block, which can be enabled and configured on the <a href="@blocks">Blocks administration page</a>. The block is available to users with the <em>Search content</em> permission.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
+ $output .= '<dt>' . t('Extending Search module') . '</dt>';
+ $output .= '<dd>' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as <a href="http://drupal.org/project/porterstemmer">Porter Stemmer</a> for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as <a href="http://drupal.org/project/apachesolr">Apache Solr</a> or <a href="http://drupal.org/project/sphinx">Sphinx</a>. These and other <a href="@contrib-search">search-related contributed modules</a> can be downloaded by visiting Drupal.org.', array('@contrib-search' => 'http://drupal.org/project/modules?filters=tid%3A105')) . '</dd>';
+ $output .= '</dl>';
+ return $output;
+ case 'admin/config/search/settings':
+ return '<p>' . t('The search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured <a href="@cron">cron maintenance task</a> is required. Indexing behavior can be adjusted using the settings below.', array('@cron' => url('admin/reports/status'))) . '</p>';
+ case 'search#noresults':
+ return t('<ul>
+<li>Check if your spelling is correct.</li>
+<li>Remove quotes around phrases to search for each word individually. <em>bike shed</em> will often show more results than <em>&quot;bike shed&quot;</em>.</li>
+<li>Consider loosening your query with <em>OR</em>. <em>bike OR shed</em> will often show more results than <em>bike shed</em>.</li>
+</ul>');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function search_theme() {
+ return array(
+ 'search_block_form' => array(
+ 'render element' => 'form',
+ 'template' => 'search-block-form',
+ ),
+ 'search_result' => array(
+ 'variables' => array('result' => NULL, 'module' => NULL),
+ 'file' => 'search.pages.inc',
+ 'template' => 'search-result',
+ ),
+ 'search_results' => array(
+ 'variables' => array('results' => NULL, 'module' => NULL),
+ 'file' => 'search.pages.inc',
+ 'template' => 'search-results',
+ ),
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function search_permission() {
+ return array(
+ 'administer search' => array(
+ 'title' => t('Administer search'),
+ ),
+ 'search content' => array(
+ 'title' => t('Use search'),
+ ),
+ 'use advanced search' => array(
+ 'title' => t('Use advanced search'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function search_block_info() {
+ $blocks['form']['info'] = t('Search form');
+ // Not worth caching.
+ $blocks['form']['cache'] = DRUPAL_NO_CACHE;
+ $blocks['form']['properties']['administrative'] = TRUE;
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function search_block_view($delta = '') {
+ if (user_access('search content')) {
+ $block['content'] = drupal_get_form('search_block_form');
+ return $block;
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function search_menu() {
+ $items['search'] = array(
+ 'title' => 'Search',
+ 'page callback' => 'search_view',
+ 'access callback' => 'search_is_active',
+ 'type' => MENU_SUGGESTED_ITEM,
+ 'file' => 'search.pages.inc',
+ );
+ $items['admin/config/search/settings'] = array(
+ 'title' => 'Search settings',
+ 'description' => 'Configure relevance settings for search and other indexing options.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('search_admin_settings'),
+ 'access arguments' => array('administer search'),
+ 'weight' => -10,
+ 'file' => 'search.admin.inc',
+ );
+ $items['admin/config/search/settings/reindex'] = array(
+ 'title' => 'Clear index',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('search_reindex_confirm'),
+ 'access arguments' => array('administer search'),
+ 'type' => MENU_VISIBLE_IN_BREADCRUMB,
+ 'file' => 'search.admin.inc',
+ );
+
+ // Add paths for searching. We add each module search path twice: once without
+ // and once with %menu_tail appended. The reason for this is that we want to
+ // preserve keywords when switching tabs, and also to have search tabs
+ // highlighted properly. The only way to do that within the Drupal menu
+ // system appears to be having two sets of tabs. See discussion on issue
+ // http://drupal.org/node/245103 for details.
+
+ drupal_static_reset('search_get_info');
+ $default_info = search_get_default_module_info();
+ if ($default_info) {
+ foreach (search_get_info() as $module => $search_info) {
+ $path = 'search/' . $search_info['path'];
+ $items[$path] = array(
+ 'title' => $search_info['title'],
+ 'page callback' => 'search_view',
+ 'page arguments' => array($module, ''),
+ 'access callback' => '_search_menu_access',
+ 'access arguments' => array($module),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'search.pages.inc',
+ 'weight' => $module == $default_info['module'] ? -10 : 0,
+ );
+ $items["$path/%menu_tail"] = array(
+ 'title' => $search_info['title'],
+ 'load arguments' => array('%map', '%index'),
+ 'page callback' => 'search_view',
+ 'page arguments' => array($module, 2),
+ 'access callback' => '_search_menu_access',
+ 'access arguments' => array($module),
+ // The default local task points to its parent, but this item points to
+ // where it should so it should not be changed.
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'search.pages.inc',
+ 'weight' => 0,
+ // These tabs are not subtabs.
+ 'tab_root' => 'search/' . $default_info['path'] . '/%',
+ // These tabs need to display at the same level.
+ 'tab_parent' => 'search/' . $default_info['path'],
+ );
+ }
+ }
+ return $items;
+}
+
+/**
+ * Determines access for the ?q=search path.
+ */
+function search_is_active() {
+ // This path cannot be accessed if there are no active modules.
+ return user_access('search content') && search_get_info();
+}
+
+/**
+ * Returns information about available search modules.
+ *
+ * @param $all
+ * If TRUE, information about all enabled modules implementing
+ * hook_search_info() will be returned. If FALSE (default), only modules that
+ * have been set to active on the search settings page will be returned.
+ *
+ * @return
+ * Array of hook_search_info() return values, keyed by module name. The
+ * 'title' and 'path' array elements will be set to defaults for each module
+ * if not supplied by hook_search_info(), and an additional array element of
+ * 'module' will be added (set to the module name).
+ */
+function search_get_info($all = FALSE) {
+ $search_hooks = &drupal_static(__FUNCTION__);
+
+ if (!isset($search_hooks)) {
+ foreach (module_implements('search_info') as $module) {
+ $search_hooks[$module] = call_user_func($module . '_search_info');
+ // Use module name as the default value.
+ $search_hooks[$module] += array('title' => $module, 'path' => $module);
+ // Include the module name itself in the array.
+ $search_hooks[$module]['module'] = $module;
+ }
+ }
+
+ if ($all) {
+ return $search_hooks;
+ }
+
+ $active = variable_get('search_active_modules', array('node', 'user'));
+ return array_intersect_key($search_hooks, array_flip($active));
+}
+
+/**
+ * Returns information about the default search module.
+ *
+ * @return
+ * The search_get_info() array element for the default search module, if any.
+ */
+function search_get_default_module_info() {
+ $info = search_get_info();
+ $default = variable_get('search_default_module', 'node');
+ if (isset($info[$default])) {
+ return $info[$default];
+ }
+ // The variable setting does not match any active module, so just return
+ // the info for the first active module (if any).
+ return reset($info);
+}
+
+/**
+ * Access callback for search tabs.
+ */
+function _search_menu_access($name) {
+ return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access'));
+}
+
+/**
+ * Clears a part of or the entire search index.
+ *
+ * @param $sid
+ * (optional) The ID of the item to remove from the search index. If
+ * specified, $module must also be given. Omit both $sid and $module to clear
+ * the entire search index.
+ * @param $module
+ * (optional) The machine-readable name of the module for the item to remove
+ * from the search index.
+ */
+function search_reindex($sid = NULL, $module = NULL, $reindex = FALSE) {
+ if ($module == NULL && $sid == NULL) {
+ module_invoke_all('search_reset');
+ }
+ else {
+ db_delete('search_dataset')
+ ->condition('sid', $sid)
+ ->condition('type', $module)
+ ->execute();
+ db_delete('search_index')
+ ->condition('sid', $sid)
+ ->condition('type', $module)
+ ->execute();
+ // Don't remove links if re-indexing.
+ if (!$reindex) {
+ db_delete('search_node_links')
+ ->condition('sid', $sid)
+ ->condition('type', $module)
+ ->execute();
+ }
+ }
+}
+
+/**
+ * Marks a word as "dirty" (changed), or retrieves the list of dirty words.
+ *
+ * This is used during indexing (cron). Words that are dirty have outdated
+ * total counts in the search_total table, and need to be recounted.
+ */
+function search_dirty($word = NULL) {
+ $dirty = &drupal_static(__FUNCTION__, array());
+ if ($word !== NULL) {
+ $dirty[$word] = TRUE;
+ }
+ else {
+ return $dirty;
+ }
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * Fires hook_update_index() in all modules and cleans up dirty words.
+ *
+ * @see search_dirty()
+ */
+function search_cron() {
+ // We register a shutdown function to ensure that search_total is always up
+ // to date.
+ drupal_register_shutdown_function('search_update_totals');
+
+ foreach (variable_get('search_active_modules', array('node', 'user')) as $module) {
+ // Update word index
+ module_invoke($module, 'update_index');
+ }
+}
+
+/**
+ * Updates the {search_total} database table.
+ *
+ * This function is called on shutdown to ensure that {search_total} is always
+ * up to date (even if cron times out or otherwise fails).
+ */
+function search_update_totals() {
+ // Update word IDF (Inverse Document Frequency) counts for new/changed words.
+ foreach (search_dirty() as $word => $dummy) {
+ // Get total count
+ $total = db_query("SELECT SUM(score) FROM {search_index} WHERE word = :word", array(':word' => $word), array('target' => 'slave'))->fetchField();
+ // Apply Zipf's law to equalize the probability distribution.
+ $total = log10(1 + 1/(max(1, $total)));
+ db_merge('search_total')
+ ->key(array('word' => $word))
+ ->fields(array('count' => $total))
+ ->execute();
+ }
+ // Find words that were deleted from search_index, but are still in
+ // search_total. We use a LEFT JOIN between the two tables and keep only the
+ // rows which fail to join.
+ $result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL", array(), array('target' => 'slave'));
+ $or = db_or();
+ foreach ($result as $word) {
+ $or->condition('word', $word->realword);
+ }
+ if (count($or) > 0) {
+ db_delete('search_total')
+ ->condition($or)
+ ->execute();
+ }
+}
+
+/**
+ * Simplifies a string according to indexing rules.
+ *
+ * @param $text
+ * Text to simplify.
+ *
+ * @return
+ * Simplified text.
+ *
+ * @see hook_search_preprocess()
+ */
+function search_simplify($text) {
+ // Decode entities to UTF-8
+ $text = decode_entities($text);
+
+ // Lowercase
+ $text = drupal_strtolower($text);
+
+ // Call an external processor for word handling.
+ search_invoke_preprocess($text);
+
+ // Simple CJK handling
+ if (variable_get('overlap_cjk', TRUE)) {
+ $text = preg_replace_callback('/[' . PREG_CLASS_CJK . ']+/u', 'search_expand_cjk', $text);
+ }
+
+ // To improve searching for numerical data such as dates, IP addresses
+ // or version numbers, we consider a group of numerical characters
+ // separated only by punctuation characters to be one piece.
+ // This also means that searching for e.g. '20/03/1984' also returns
+ // results with '20-03-1984' in them.
+ // Readable regexp: ([number]+)[punctuation]+(?=[number])
+ $text = preg_replace('/([' . PREG_CLASS_NUMBERS . ']+)[' . PREG_CLASS_PUNCTUATION . ']+(?=[' . PREG_CLASS_NUMBERS . '])/u', '\1', $text);
+
+ // Multiple dot and dash groups are word boundaries and replaced with space.
+ // No need to use the unicode modifer here because 0-127 ASCII characters
+ // can't match higher UTF-8 characters as the leftmost bit of those are 1.
+ $text = preg_replace('/[.-]{2,}/', ' ', $text);
+
+ // The dot, underscore and dash are simply removed. This allows meaningful
+ // search behavior with acronyms and URLs. See unicode note directly above.
+ $text = preg_replace('/[._-]+/', '', $text);
+
+ // With the exception of the rules above, we consider all punctuation,
+ // marks, spacers, etc, to be a word boundary.
+ $text = preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', ' ', $text);
+
+ // Truncate everything to 50 characters.
+ $words = explode(' ', $text);
+ array_walk($words, '_search_index_truncate');
+ $text = implode(' ', $words);
+
+ return $text;
+}
+
+/**
+ * Splits CJK (Chinese, Japanese, Korean) text into tokens.
+ *
+ * The Search module matches exact words, where a word is defined to be a
+ * sequence of characters delimited by spaces or punctuation. CJK languages are
+ * written in long strings of characters, though, not split up into words. So
+ * in order to allow search matching, we split up CJK text into tokens
+ * consisting of consecutive, overlapping sequences of characters whose length
+ * is equal to the 'minimum_word_size' variable. This tokenizing is only done if
+ * the 'overlap_cjk' variable is TRUE.
+ *
+ * @param $matches
+ * This function is a callback for preg_replace_callback(), which is called
+ * from search_simplify(). So, $matches is an array of regular expression
+ * matches, which means that $matches[0] contains the matched text -- a string
+ * of CJK characters to tokenize.
+ *
+ * @return
+ * Tokenized text, starting and ending with a space character.
+ */
+function search_expand_cjk($matches) {
+ $min = variable_get('minimum_word_size', 3);
+ $str = $matches[0];
+ $length = drupal_strlen($str);
+ // If the text is shorter than the minimum word size, don't tokenize it.
+ if ($length <= $min) {
+ return ' ' . $str . ' ';
+ }
+ $tokens = ' ';
+ // Build a FIFO queue of characters.
+ $chars = array();
+ for ($i = 0; $i < $length; $i++) {
+ // Add the next character off the beginning of the string to the queue.
+ $current = drupal_substr($str, 0, 1);
+ $str = substr($str, strlen($current));
+ $chars[] = $current;
+ if ($i >= $min - 1) {
+ // Make a token of $min characters, and add it to the token string.
+ $tokens .= implode('', $chars) . ' ';
+ // Shift out the first character in the queue.
+ array_shift($chars);
+ }
+ }
+ return $tokens;
+}
+
+/**
+ * Simplifies and splits a string into tokens for indexing.
+ */
+function search_index_split($text) {
+ $last = &drupal_static(__FUNCTION__);
+ $lastsplit = &drupal_static(__FUNCTION__ . ':lastsplit');
+
+ if ($last == $text) {
+ return $lastsplit;
+ }
+ // Process words
+ $text = search_simplify($text);
+ $words = explode(' ', $text);
+
+ // Save last keyword result
+ $last = $text;
+ $lastsplit = $words;
+
+ return $words;
+}
+
+/**
+ * Helper function for array_walk in search_index_split.
+ */
+function _search_index_truncate(&$text) {
+ if (is_numeric($text)) {
+ $text = ltrim($text, '0');
+ }
+ $text = truncate_utf8($text, 50);
+}
+
+/**
+ * Invokes hook_search_preprocess() in modules.
+ */
+function search_invoke_preprocess(&$text) {
+ foreach (module_implements('search_preprocess') as $module) {
+ $text = module_invoke($module, 'search_preprocess', $text);
+ }
+}
+
+/**
+ * Update the full-text search index for a particular item.
+ *
+ * @param $sid
+ * An ID number identifying this particular item (e.g., node ID).
+ * @param $module
+ * The machine-readable name of the module that this item comes from (a module
+ * that implements hook_search_info()).
+ * @param $text
+ * The content of this item. Must be a piece of HTML or plain text.
+ *
+ * @ingroup search
+ */
+function search_index($sid, $module, $text) {
+ $minimum_word_size = variable_get('minimum_word_size', 3);
+
+ // Link matching
+ global $base_url;
+ $node_regexp = '@href=[\'"]?(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i';
+
+ // Multipliers for scores of words inside certain HTML tags. The weights are stored
+ // in a variable so that modules can overwrite the default weights.
+ // Note: 'a' must be included for link ranking to work.
+ $tags = variable_get('search_tag_weights', array(
+ 'h1' => 25,
+ 'h2' => 18,
+ 'h3' => 15,
+ 'h4' => 12,
+ 'h5' => 9,
+ 'h6' => 6,
+ 'u' => 3,
+ 'b' => 3,
+ 'i' => 3,
+ 'strong' => 3,
+ 'em' => 3,
+ 'a' => 10));
+
+ // Strip off all ignored tags to speed up processing, but insert space before/after
+ // them to keep word boundaries.
+ $text = str_replace(array('<', '>'), array(' <', '> '), $text);
+ $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>');
+
+ // Split HTML tags from plain text.
+ $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Note: PHP ensures the array consists of alternating delimiters and literals
+ // and begins and ends with a literal (inserting $null as required).
+
+ $tag = FALSE; // Odd/even counter. Tag or no tag.
+ $link = FALSE; // State variable for link analyzer
+ $score = 1; // Starting score per word
+ $accum = ' '; // Accumulator for cleaned up data
+ $tagstack = array(); // Stack with open tags
+ $tagwords = 0; // Counter for consecutive words
+ $focus = 1; // Focus state
+
+ $results = array(0 => array()); // Accumulator for words for index
+
+ foreach ($split as $value) {
+ if ($tag) {
+ // Increase or decrease score per word based on tag
+ list($tagname) = explode(' ', $value, 2);
+ $tagname = drupal_strtolower($tagname);
+ // Closing or opening tag?
+ if ($tagname[0] == '/') {
+ $tagname = substr($tagname, 1);
+ // If we encounter unexpected tags, reset score to avoid incorrect boosting.
+ if (!count($tagstack) || $tagstack[0] != $tagname) {
+ $tagstack = array();
+ $score = 1;
+ }
+ else {
+ // Remove from tag stack and decrement score
+ $score = max(1, $score - $tags[array_shift($tagstack)]);
+ }
+ if ($tagname == 'a') {
+ $link = FALSE;
+ }
+ }
+ else {
+ if (isset($tagstack[0]) && $tagstack[0] == $tagname) {
+ // None of the tags we look for make sense when nested identically.
+ // If they are, it's probably broken HTML.
+ $tagstack = array();
+ $score = 1;
+ }
+ else {
+ // Add to open tag stack and increment score
+ array_unshift($tagstack, $tagname);
+ $score += $tags[$tagname];
+ }
+ if ($tagname == 'a') {
+ // Check if link points to a node on this site
+ if (preg_match($node_regexp, $value, $match)) {
+ $path = drupal_get_normal_path($match[1]);
+ if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) {
+ $linknid = $match[1];
+ if ($linknid > 0) {
+ $node = db_query('SELECT title, nid, vid FROM {node} WHERE nid = :nid', array(':nid' => $linknid), array('target' => 'slave'))->fetchObject();
+ $link = TRUE;
+ $linktitle = $node->title;
+ }
+ }
+ }
+ }
+ }
+ // A tag change occurred, reset counter.
+ $tagwords = 0;
+ }
+ else {
+ // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values
+ if ($value != '') {
+ if ($link) {
+ // Check to see if the node link text is its URL. If so, we use the target node title instead.
+ if (preg_match('!^https?://!i', $value)) {
+ $value = $linktitle;
+ }
+ }
+ $words = search_index_split($value);
+ foreach ($words as $word) {
+ // Add word to accumulator
+ $accum .= $word . ' ';
+ // Check wordlength
+ if (is_numeric($word) || drupal_strlen($word) >= $minimum_word_size) {
+ // Links score mainly for the target.
+ if ($link) {
+ if (!isset($results[$linknid])) {
+ $results[$linknid] = array();
+ }
+ $results[$linknid][] = $word;
+ // Reduce score of the link caption in the source.
+ $focus *= 0.2;
+ }
+ // Fall-through
+ if (!isset($results[0][$word])) {
+ $results[0][$word] = 0;
+ }
+ $results[0][$word] += $score * $focus;
+
+ // Focus is a decaying value in terms of the amount of unique words up to this point.
+ // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words.
+ $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015));
+ }
+ $tagwords++;
+ // Too many words inside a single tag probably mean a tag was accidentally left open.
+ if (count($tagstack) && $tagwords >= 15) {
+ $tagstack = array();
+ $score = 1;
+ }
+ }
+ }
+ }
+ $tag = !$tag;
+ }
+
+ search_reindex($sid, $module, TRUE);
+
+ // Insert cleaned up data into dataset
+ db_insert('search_dataset')
+ ->fields(array(
+ 'sid' => $sid,
+ 'type' => $module,
+ 'data' => $accum,
+ 'reindex' => 0,
+ ))
+ ->execute();
+
+ // Insert results into search index
+ foreach ($results[0] as $word => $score) {
+ // If a word already exists in the database, its score gets increased
+ // appropriately. If not, we create a new record with the appropriate
+ // starting score.
+ db_merge('search_index')
+ ->key(array(
+ 'word' => $word,
+ 'sid' => $sid,
+ 'type' => $module,
+ ))
+ ->fields(array('score' => $score))
+ ->expression('score', 'score + :score', array(':score' => $score))
+ ->execute();
+ search_dirty($word);
+ }
+ unset($results[0]);
+
+ // Get all previous links from this item.
+ $result = db_query("SELECT nid, caption FROM {search_node_links} WHERE sid = :sid AND type = :type", array(
+ ':sid' => $sid,
+ ':type' => $module
+ ), array('target' => 'slave'));
+ $links = array();
+ foreach ($result as $link) {
+ $links[$link->nid] = $link->caption;
+ }
+
+ // Now store links to nodes.
+ foreach ($results as $nid => $words) {
+ $caption = implode(' ', $words);
+ if (isset($links[$nid])) {
+ if ($links[$nid] != $caption) {
+ // Update the existing link and mark the node for reindexing.
+ db_update('search_node_links')
+ ->fields(array('caption' => $caption))
+ ->condition('sid', $sid)
+ ->condition('type', $module)
+ ->condition('nid', $nid)
+ ->execute();
+ search_touch_node($nid);
+ }
+ // Unset the link to mark it as processed.
+ unset($links[$nid]);
+ }
+ elseif ($sid != $nid || $module != 'node') {
+ // Insert the existing link and mark the node for reindexing, but don't
+ // reindex if this is a link in a node pointing to itself.
+ db_insert('search_node_links')
+ ->fields(array(
+ 'caption' => $caption,
+ 'sid' => $sid,
+ 'type' => $module,
+ 'nid' => $nid,
+ ))
+ ->execute();
+ search_touch_node($nid);
+ }
+ }
+ // Any left-over links in $links no longer exist. Delete them and mark the nodes for reindexing.
+ foreach ($links as $nid => $caption) {
+ db_delete('search_node_links')
+ ->condition('sid', $sid)
+ ->condition('type', $module)
+ ->condition('nid', $nid)
+ ->execute();
+ search_touch_node($nid);
+ }
+}
+
+/**
+ * Changes a node's changed timestamp to 'now' to force reindexing.
+ *
+ * @param $nid
+ * The node ID of the node that needs reindexing.
+ */
+function search_touch_node($nid) {
+ db_update('search_dataset')
+ ->fields(array('reindex' => REQUEST_TIME))
+ ->condition('type', 'node')
+ ->condition('sid', $nid)
+ ->execute();
+}
+
+/**
+ * Implements hook_node_update_index().
+ */
+function search_node_update_index($node) {
+ // Transplant links to a node into the target node.
+ $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = :nid", array(':nid' => $node->nid), array('target' => 'slave'));
+ $output = array();
+ foreach ($result as $link) {
+ $output[] = $link->caption;
+ }
+ if (count($output)) {
+ return '<a>(' . implode(', ', $output) . ')</a>';
+ }
+}
+
+/**
+ * Implements hook_node_update().
+ */
+function search_node_update($node) {
+ // Reindex the node when it is updated. The node is automatically indexed
+ // when it is added, simply by being added to the node table.
+ search_touch_node($node->nid);
+}
+
+/**
+ * Implements hook_comment_insert().
+ */
+function search_comment_insert($comment) {
+ // Reindex the node when comments are added.
+ search_touch_node($comment->nid);
+}
+
+/**
+ * Implements hook_comment_update().
+ */
+function search_comment_update($comment) {
+ // Reindex the node when comments are changed.
+ search_touch_node($comment->nid);
+}
+
+/**
+ * Implements hook_comment_delete().
+ */
+function search_comment_delete($comment) {
+ // Reindex the node when comments are deleted.
+ search_touch_node($comment->nid);
+}
+
+/**
+ * Implements hook_comment_publish().
+ */
+function search_comment_publish($comment) {
+ // Reindex the node when comments are published.
+ search_touch_node($comment->nid);
+}
+
+/**
+ * Implements hook_comment_unpublish().
+ */
+function search_comment_unpublish($comment) {
+ // Reindex the node when comments are unpublished.
+ search_touch_node($comment->nid);
+}
+
+/**
+ * Extracts a module-specific search option from a search expression.
+ *
+ * Search options are added using search_expression_insert(), and retrieved
+ * using search_expression_extract(). They take the form option:value, and
+ * are added to the ordinary keywords in the search expression.
+ *
+ * @param $expression
+ * The search expression to extract from.
+ * @param $option
+ * The name of the option to retrieve from the search expression.
+ *
+ * @return
+ * The value previously stored in the search expression for option $option,
+ * if any. Trailing spaces in values will not be included.
+ */
+function search_expression_extract($expression, $option) {
+ if (preg_match('/(^| )' . $option . ':([^ ]*)( |$)/i', $expression, $matches)) {
+ return $matches[2];
+ }
+}
+
+/**
+ * Adds a module-specific search option to a search expression.
+ *
+ * Search options are added using search_expression_insert(), and retrieved
+ * using search_expression_extract(). They take the form option:value, and
+ * are added to the ordinary keywords in the search expression.
+ *
+ * @param $expression
+ * The search expression to add to.
+ * @param $option
+ * The name of the option to add to the search expression.
+ * @param $value
+ * The value to add for the option. If present, it will replace any previous
+ * value added for the option. Cannot contain any spaces or | characters, as
+ * these are used as delimiters. If you want to add a blank value $option: to
+ * the search expression, pass in an empty string or a string that is composed
+ * of only spaces. To clear a previously-stored option without adding a
+ * replacement, pass in NULL for $value or omit.
+ *
+ * @return
+ * $expression, with any previous value for this option removed, and a new
+ * $option:$value pair added if $value was provided.
+ */
+function search_expression_insert($expression, $option, $value = NULL) {
+ // Remove any previous values stored with $option.
+ $expression = trim(preg_replace('/(^| )' . $option . ':[^ ]*/i', '', $expression));
+
+ // Set new value, if provided.
+ if (isset($value)) {
+ $expression .= ' ' . $option . ':' . trim($value);
+ }
+ return $expression;
+}
+
+/**
+ * @defgroup search Search interface
+ * @{
+ * The Drupal search interface manages a global search mechanism.
+ *
+ * Modules may plug into this system to provide searches of different types of
+ * data. Most of the system is handled by search.module, so this must be enabled
+ * for all of the search features to work.
+ *
+ * There are three ways to interact with the search system:
+ * - Specifically for searching nodes, you can implement
+ * hook_node_update_index() and hook_node_search_result(). However, note that
+ * the search system already indexes all visible output of a node; i.e.,
+ * everything displayed normally by hook_view() and hook_node_view(). This is
+ * usually sufficient. You should only use this mechanism if you want
+ * additional, non-visible data to be indexed.
+ * - Implement hook_search_info(). This will create a search tab for your module
+ * on the /search page with a simple keyword search form. You will also need
+ * to implement hook_search_execute() to perform the search.
+ * - Implement hook_update_index(). This allows your module to use Drupal's
+ * HTML indexing mechanism for searching full text efficiently.
+ *
+ * If your module needs to provide a more complicated search form, then you need
+ * to implement it yourself without hook_search_info(). In that case, you should
+ * define it as a local task (tab) under the /search page (e.g. /search/mymodule)
+ * so that users can easily find it.
+ */
+
+/**
+ * Builds a search form.
+ *
+ * @param $action
+ * Form action. Defaults to "search/$path", where $path is the search path
+ * associated with the module in its hook_search_info(). This will be
+ * run through url().
+ * @param $keys
+ * The search string entered by the user, containing keywords for the search.
+ * @param $module
+ * The search module to render the form for: a module that implements
+ * hook_search_info(). If not supplied, the default search module is used.
+ * @param $prompt
+ * Label for the keywords field. Defaults to t('Enter your keywords') if NULL.
+ * Supply '' to omit.
+ *
+ * @return
+ * A Form API array for the search form.
+ */
+function search_form($form, &$form_state, $action = '', $keys = '', $module = NULL, $prompt = NULL) {
+ $module_info = FALSE;
+ if (!$module) {
+ $module_info = search_get_default_module_info();
+ }
+ else {
+ $info = search_get_info();
+ $module_info = isset($info[$module]) ? $info[$module] : FALSE;
+ }
+
+ // Sanity check.
+ if (!$module_info) {
+ form_set_error(NULL, t('Search is currently disabled.'), 'error');
+ return $form;
+ }
+
+ if (!$action) {
+ $action = 'search/' . $module_info['path'];
+ }
+ if (!isset($prompt)) {
+ $prompt = t('Enter your keywords');
+ }
+
+ $form['#action'] = url($action);
+ // Record the $action for later use in redirecting.
+ $form_state['action'] = $action;
+ $form['#attributes']['class'][] = 'search-form';
+ $form['module'] = array('#type' => 'value', '#value' => $module);
+ $form['basic'] = array('#type' => 'container', '#attributes' => array('class' => array('container-inline')));
+ $form['basic']['keys'] = array(
+ '#type' => 'textfield',
+ '#title' => $prompt,
+ '#default_value' => $keys,
+ '#size' => $prompt ? 40 : 20,
+ '#maxlength' => 255,
+ );
+ // processed_keys is used to coordinate keyword passing between other forms
+ // that hook into the basic search form.
+ $form['basic']['processed_keys'] = array('#type' => 'value', '#value' => '');
+ $form['basic']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+
+ return $form;
+}
+
+/**
+ * Form builder; Output a search form for the search block's search box.
+ *
+ * @ingroup forms
+ * @see search_box_form_submit()
+ * @see search-block-form.tpl.php
+ */
+function search_box($form, &$form_state, $form_id) {
+ $form[$form_id] = array(
+ '#type' => 'textfield',
+ '#title' => t('Search'),
+ '#title_display' => 'invisible',
+ '#size' => 15,
+ '#default_value' => '',
+ '#attributes' => array('title' => t('Enter the terms you wish to search for.')),
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+ $form['#submit'][] = 'search_box_form_submit';
+
+ return $form;
+}
+
+/**
+ * Process a block search form submission.
+ */
+function search_box_form_submit($form, &$form_state) {
+ // The search form relies on control of the redirect destination for its
+ // functionality, so we override any static destination set in the request,
+ // for example by drupal_access_denied() or drupal_not_found()
+ // (see http://drupal.org/node/292565).
+ if (isset($_GET['destination'])) {
+ unset($_GET['destination']);
+ }
+
+ // Check to see if the form was submitted empty.
+ // If it is empty, display an error message.
+ // (This method is used instead of setting #required to TRUE for this field
+ // because that results in a confusing error message. It would say a plain
+ // "field is required" because the search keywords field has no title.
+ // The error message would also complain about a missing #title field.)
+ if ($form_state['values']['search_block_form'] == '') {
+ form_set_error('keys', t('Please enter some keywords.'));
+ }
+
+ $form_id = $form['form_id']['#value'];
+ $info = search_get_default_module_info();
+ if ($info) {
+ $form_state['redirect'] = 'search/' . $info['path'] . '/' . trim($form_state['values'][$form_id]);
+ }
+ else {
+ form_set_error(NULL, t('Search is currently disabled.'), 'error');
+ }
+}
+
+/**
+ * Process variables for search-block-form.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $form
+ *
+ * @see search-block-form.tpl.php
+ */
+function template_preprocess_search_block_form(&$variables) {
+ $variables['search'] = array();
+ $hidden = array();
+ // Provide variables named after form keys so themers can print each element independently.
+ foreach (element_children($variables['form']) as $key) {
+ $type = isset($variables['form'][$key]['#type']) ? $variables['form'][$key]['#type'] : '';
+ if ($type == 'hidden' || $type == 'token') {
+ $hidden[] = drupal_render($variables['form'][$key]);
+ }
+ else {
+ $variables['search'][$key] = drupal_render($variables['form'][$key]);
+ }
+ }
+ // Hidden form elements have no value to themers. No need for separation.
+ $variables['search']['hidden'] = implode($hidden);
+ // Collect all form elements to make it easier to print the whole form.
+ $variables['search_form'] = implode($variables['search']);
+}
+
+/**
+ * Performs a search by calling hook_search_execute().
+ *
+ * @param $keys
+ * Keyword query to search on.
+ * @param $module
+ * Search module to search.
+ * @param $conditions
+ * Optional array of additional search conditions.
+ *
+ * @return
+ * Renderable array of search results. No return value if $keys are not
+ * supplied or if the given search module is not active.
+ */
+function search_data($keys, $module, $conditions = NULL) {
+ if (module_hook($module, 'search_execute')) {
+ $results = module_invoke($module, 'search_execute', $keys, $conditions);
+ if (module_hook($module, 'search_page')) {
+ return module_invoke($module, 'search_page', $results);
+ }
+ else {
+ return array(
+ '#theme' => 'search_results',
+ '#results' => $results,
+ '#module' => $module,
+ );
+ }
+ }
+}
+
+/**
+ * Returns snippets from a piece of text, with certain keywords highlighted.
+ * Used for formatting search results.
+ *
+ * @param $keys
+ * A string containing a search query.
+ *
+ * @param $text
+ * The text to extract fragments from.
+ *
+ * @return
+ * A string containing HTML for the excerpt.
+ */
+function search_excerpt($keys, $text) {
+ // We highlight around non-indexable or CJK characters.
+ $boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . PREG_CLASS_CJK . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . PREG_CLASS_CJK . ']))';
+
+ // Extract positive keywords and phrases
+ preg_match_all('/ ("([^"]+)"|(?!OR)([^" ]+))/', ' ' . $keys, $matches);
+ $keys = array_merge($matches[2], $matches[3]);
+
+ // Prepare text by stripping HTML tags and decoding HTML entities.
+ $text = strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text));
+ $text = decode_entities($text);
+
+ // Slash-escape quotes in the search keyword string.
+ array_walk($keys, '_search_excerpt_replace');
+ $workkeys = $keys;
+
+ // Extract fragments around keywords.
+ // First we collect ranges of text around each keyword, starting/ending
+ // at spaces, trying to get to 256 characters.
+ // If the sum of all fragments is too short, we look for second occurrences.
+ $ranges = array();
+ $included = array();
+ $foundkeys = array();
+ $length = 0;
+ while ($length < 256 && count($workkeys)) {
+ foreach ($workkeys as $k => $key) {
+ if (strlen($key) == 0) {
+ unset($workkeys[$k]);
+ unset($keys[$k]);
+ continue;
+ }
+ if ($length >= 256) {
+ break;
+ }
+ // Remember occurrence of key so we can skip over it if more occurrences
+ // are desired.
+ if (!isset($included[$key])) {
+ $included[$key] = 0;
+ }
+ // Locate a keyword (position $p, always >0 because $text starts with a
+ // space). First try bare keyword, but if that doesn't work, try to find a
+ // derived form from search_simplify().
+ $p = 0;
+ if (preg_match('/' . $boundary . $key . $boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
+ $p = $match[0][1];
+ }
+ else {
+ $info = search_simplify_excerpt_match($key, $text, $included[$key], $boundary);
+ if ($info['where']) {
+ $p = $info['where'];
+ if ($info['keyword']) {
+ $foundkeys[] = $info['keyword'];
+ }
+ }
+ }
+ // Now locate a space in front (position $q) and behind it (position $s),
+ // leaving about 60 characters extra before and after for context.
+ // Note that a space was added to the front and end of $text above.
+ if ($p) {
+ if (($q = strpos(' ' . $text, ' ', max(0, $p - 61))) !== FALSE) {
+ $end = substr($text . ' ', $p, 80);
+ if (($s = strrpos($end, ' ')) !== FALSE) {
+ // Account for the added spaces.
+ $q = max($q - 1, 0);
+ $s = min($s, strlen($end) - 1);
+ $ranges[$q] = $p + $s;
+ $length += $p + $s - $q;
+ $included[$key] = $p + 1;
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ }
+
+ if (count($ranges) == 0) {
+ // We didn't find any keyword matches, so just return the first part of the
+ // text. We also need to re-encode any HTML special characters that we
+ // entity-decoded above.
+ return check_plain(truncate_utf8($text, 256, TRUE, TRUE));
+ }
+
+ // Sort the text ranges by starting position.
+ ksort($ranges);
+
+ // Now we collapse overlapping text ranges into one. The sorting makes it O(n).
+ $newranges = array();
+ foreach ($ranges as $from2 => $to2) {
+ if (!isset($from1)) {
+ $from1 = $from2;
+ $to1 = $to2;
+ continue;
+ }
+ if ($from2 <= $to1) {
+ $to1 = max($to1, $to2);
+ }
+ else {
+ $newranges[$from1] = $to1;
+ $from1 = $from2;
+ $to1 = $to2;
+ }
+ }
+ $newranges[$from1] = $to1;
+
+ // Fetch text
+ $out = array();
+ foreach ($newranges as $from => $to) {
+ $out[] = substr($text, $from, $to - $from);
+ }
+
+ // Let translators have the ... separator text as one chunk.
+ $dots = explode('!excerpt', t('... !excerpt ... !excerpt ...'));
+
+ $text = (isset($newranges[0]) ? '' : $dots[0]) . implode($dots[1], $out) . $dots[2];
+ $text = check_plain($text);
+
+ // Slash-escape quotes in keys found in a derived form and merge with original keys.
+ array_walk($foundkeys, '_search_excerpt_replace');
+ $keys = array_merge($keys, $foundkeys);
+
+ // Highlight keywords. Must be done at once to prevent conflicts ('strong' and '<strong>').
+ $text = preg_replace('/' . $boundary . '(' . implode('|', $keys) . ')' . $boundary . '/iu', '<strong>\0</strong>', $text);
+ return $text;
+}
+
+/**
+ * @} End of "defgroup search".
+ */
+
+/**
+ * Helper function for array_walk() in search_excerpt().
+ */
+function _search_excerpt_replace(&$text) {
+ $text = preg_quote($text, '/');
+}
+
+/**
+ * Find words in the original text that matched via search_simplify().
+ *
+ * This is called in search_excerpt() if an exact match is not found in the
+ * text, so that we can find the derived form that matches.
+ *
+ * @param $key
+ * The keyword to find.
+ * @param $text
+ * The text to search for the keyword.
+ * @param $offset
+ * Offset position in $text to start searching at.
+ * @param $boundary
+ * Text to include in a regular expression that will match a word boundary.
+ *
+ * @return
+ * FALSE if no match is found. If a match is found, return an associative
+ * array with element 'where' giving the position of the match, and element
+ * 'keyword' giving the actual word found in the text at that position.
+ */
+function search_simplify_excerpt_match($key, $text, $offset, $boundary) {
+ $pos = NULL;
+ $simplified_key = search_simplify($key);
+ $simplified_text = search_simplify($text);
+
+ // Return immediately if simplified key or text are empty.
+ if (!$simplified_key || !$simplified_text) {
+ return FALSE;
+ }
+
+ // Check if we have a match after simplification in the text.
+ if (!preg_match('/' . $boundary . $simplified_key . $boundary . '/iu', $simplified_text, $match, PREG_OFFSET_CAPTURE, $offset)) {
+ return FALSE;
+ }
+
+ // If we get here, we have a match. Now find the exact location of the match
+ // and the original text that matched. Start by splitting up the text by all
+ // potential starting points of the matching text and iterating through them.
+ $split = array_filter(preg_split('/' . $boundary . '/iu', $text, -1, PREG_SPLIT_OFFSET_CAPTURE), '_search_excerpt_match_filter');
+ foreach ($split as $value) {
+ // Skip starting points before the offset.
+ if ($value[1] < $offset) {
+ continue;
+ }
+
+ // Check a window of 80 characters after the starting point for a match,
+ // based on the size of the excerpt window.
+ $window = substr($text, $value[1], 80);
+ $simplified_window = search_simplify($window);
+ if (strpos($simplified_window, $simplified_key) === 0) {
+ // We have a match in this window. Store the position of the match.
+ $pos = $value[1];
+ // Iterate through the text in the window until we find the full original
+ // matching text.
+ $length = strlen($window);
+ for ($i = 1; $i <= $length; $i++) {
+ $keyfound = substr($text, $value[1], $i);
+ if ($simplified_key == search_simplify($keyfound)) {
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ return $pos ? array('where' => $pos, 'keyword' => $keyfound) : FALSE;
+}
+
+/**
+ * Helper function for array_filter() in search_search_excerpt_match().
+ */
+function _search_excerpt_match_filter($var) {
+ return strlen(trim($var[0]));
+}
+
+/**
+ * Implements hook_forms().
+ */
+function search_forms() {
+ $forms['search_block_form']= array(
+ 'callback' => 'search_box',
+ 'callback arguments' => array('search_block_form'),
+ );
+ return $forms;
+}
+
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.pages.inc b/kolab.org/www/drupal-7.26/modules/search/search.pages.inc
new file mode 100644
index 0000000..9dd00a6
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.pages.inc
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * @file
+ * User page callbacks for the search module.
+ */
+
+/**
+ * Menu callback; presents the search form and/or search results.
+ *
+ * @param $module
+ * Search module to use for the search.
+ * @param $keys
+ * Keywords to use for the search.
+ */
+function search_view($module = NULL, $keys = '') {
+ $info = FALSE;
+ $keys = trim($keys);
+ // Also try to pull search keywords out of the $_REQUEST variable to
+ // support old GET format of searches for existing links.
+ if (!$keys && !empty($_REQUEST['keys'])) {
+ $keys = trim($_REQUEST['keys']);
+ }
+
+ if (!empty($module)) {
+ $active_module_info = search_get_info();
+ if (isset($active_module_info[$module])) {
+ $info = $active_module_info[$module];
+ }
+ }
+
+ if (empty($info)) {
+ // No path or invalid path: find the default module. Note that if there
+ // are no enabled search modules, this function should never be called,
+ // since hook_menu() would not have defined any search paths.
+ $info = search_get_default_module_info();
+ // Redirect from bare /search or an invalid path to the default search path.
+ $path = 'search/' . $info['path'];
+ if ($keys) {
+ $path .= '/' . $keys;
+ }
+ drupal_goto($path);
+ }
+
+ // Default results output is an empty string.
+ $results = array('#markup' => '');
+ // Process the search form. Note that if there is $_POST data,
+ // search_form_submit() will cause a redirect to search/[module path]/[keys],
+ // which will get us back to this page callback. In other words, the search
+ // form submits with POST but redirects to GET. This way we can keep
+ // the search query URL clean as a whistle.
+ if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') {
+ $conditions = NULL;
+ if (isset($info['conditions_callback']) && function_exists($info['conditions_callback'])) {
+ // Build an optional array of more search conditions.
+ $conditions = call_user_func($info['conditions_callback'], $keys);
+ }
+ // Only search if there are keywords or non-empty conditions.
+ if ($keys || !empty($conditions)) {
+ // Log the search keys.
+ watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys));
+
+ // Collect the search results.
+ $results = search_data($keys, $info['module'], $conditions);
+ }
+ }
+ // The form may be altered based on whether the search was run.
+ $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']);
+ $build['search_results'] = $results;
+
+ return $build;
+}
+
+/**
+ * Process variables for search-results.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $results: Search results array.
+ * - $module: Module the search results came from (module implementing
+ * hook_search_info()).
+ *
+ * @see search-results.tpl.php
+ */
+function template_preprocess_search_results(&$variables) {
+ $variables['search_results'] = '';
+ if (!empty($variables['module'])) {
+ $variables['module'] = check_plain($variables['module']);
+ }
+ foreach ($variables['results'] as $result) {
+ $variables['search_results'] .= theme('search_result', array('result' => $result, 'module' => $variables['module']));
+ }
+ $variables['pager'] = theme('pager', array('tags' => NULL));
+ $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'];
+}
+
+/**
+ * Process variables for search-result.tpl.php.
+ *
+ * The $variables array contains the following arguments:
+ * - $result
+ * - $module
+ *
+ * @see search-result.tpl.php
+ */
+function template_preprocess_search_result(&$variables) {
+ global $language;
+
+ $result = $variables['result'];
+ $variables['url'] = check_url($result['link']);
+ $variables['title'] = check_plain($result['title']);
+ if (isset($result['language']) && $result['language'] != $language->language && $result['language'] != LANGUAGE_NONE) {
+ $variables['title_attributes_array']['xml:lang'] = $result['language'];
+ $variables['content_attributes_array']['xml:lang'] = $result['language'];
+ }
+
+ $info = array();
+ if (!empty($result['module'])) {
+ $info['module'] = check_plain($result['module']);
+ }
+ if (!empty($result['user'])) {
+ $info['user'] = $result['user'];
+ }
+ if (!empty($result['date'])) {
+ $info['date'] = format_date($result['date'], 'short');
+ }
+ if (isset($result['extra']) && is_array($result['extra'])) {
+ $info = array_merge($info, $result['extra']);
+ }
+ // Check for existence. User search does not include snippets.
+ $variables['snippet'] = isset($result['snippet']) ? $result['snippet'] : '';
+ // Provide separated and grouped meta information..
+ $variables['info_split'] = $info;
+ $variables['info'] = implode(' - ', $info);
+ $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['module'];
+}
+
+/**
+ * As the search form collates keys from other modules hooked in via
+ * hook_form_alter, the validation takes place in _submit.
+ * search_form_validate() is used solely to set the 'processed_keys' form
+ * value for the basic search form.
+ */
+function search_form_validate($form, &$form_state) {
+ form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state);
+}
+
+/**
+ * Process a search form submission.
+ */
+function search_form_submit($form, &$form_state) {
+ $keys = $form_state['values']['processed_keys'];
+ if ($keys == '') {
+ form_set_error('keys', t('Please enter some keywords.'));
+ // Fall through to the form redirect.
+ }
+
+ $form_state['redirect'] = $form_state['action'] . '/' . $keys;
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/search.test b/kolab.org/www/drupal-7.26/modules/search/search.test
new file mode 100644
index 0000000..09c879b
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/search.test
@@ -0,0 +1,2079 @@
+<?php
+
+/**
+ * @file
+ * Tests for search.module.
+ */
+
+// The search index can contain different types of content. Typically the type is 'node'.
+// Here we test with _test_ and _test2_ as the type.
+define('SEARCH_TYPE', '_test_');
+define('SEARCH_TYPE_2', '_test2_');
+define('SEARCH_TYPE_JPN', '_test3_');
+
+class SearchMatchTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search engine queries',
+ 'description' => 'Indexes content and queries it.',
+ 'group' => 'Search',
+ );
+ }
+
+ /**
+ * Implementation setUp().
+ */
+ function setUp() {
+ parent::setUp('search');
+ }
+
+ /**
+ * Test search indexing.
+ */
+ function testMatching() {
+ $this->_setup();
+ $this->_testQueries();
+ }
+
+ /**
+ * Set up a small index of items to test against.
+ */
+ function _setup() {
+ variable_set('minimum_word_size', 3);
+
+ for ($i = 1; $i <= 7; ++$i) {
+ search_index($i, SEARCH_TYPE, $this->getText($i));
+ }
+ for ($i = 1; $i <= 5; ++$i) {
+ search_index($i + 7, SEARCH_TYPE_2, $this->getText2($i));
+ }
+ // No getText builder function for Japanese text; just a simple array.
+ foreach (array(
+ 13 => '以呂波耳・ほへとち。リヌルヲ。',
+ 14 => 'ドルーパルが大好きよ!',
+ 15 => 'コーヒーとケーキ',
+ ) as $i => $jpn) {
+ search_index($i, SEARCH_TYPE_JPN, $jpn);
+ }
+ search_update_totals();
+ }
+
+ /**
+ * _test_: Helper method for generating snippets of content.
+ *
+ * Generated items to test against:
+ * 1 ipsum
+ * 2 dolore sit
+ * 3 sit am ut
+ * 4 am ut enim am
+ * 5 ut enim am minim veniam
+ * 6 enim am minim veniam es cillum
+ * 7 am minim veniam es cillum dolore eu
+ */
+ function getText($n) {
+ $words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
+ return implode(' ', array_slice($words, $n - 1, $n));
+ }
+
+ /**
+ * _test2_: Helper method for generating snippets of content.
+ *
+ * Generated items to test against:
+ * 8 dear
+ * 9 king philip
+ * 10 philip came over
+ * 11 came over from germany
+ * 12 over from germany swimming
+ */
+ function getText2($n) {
+ $words = explode(' ', "Dear King Philip came over from Germany swimming.");
+ return implode(' ', array_slice($words, $n - 1, $n));
+ }
+
+ /**
+ * Run predefine queries looking for indexed terms.
+ */
+ function _testQueries() {
+ /*
+ Note: OR queries that include short words in OR groups are only accepted
+ if the ORed terms are ANDed with at least one long word in the rest of the query.
+
+ e.g. enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut) -> good
+ e.g. dolore OR ut = (dolore) OR (ut) -> bad
+
+ This is a design limitation to avoid full table scans.
+ */
+ $queries = array(
+ // Simple AND queries.
+ 'ipsum' => array(1),
+ 'enim' => array(4, 5, 6),
+ 'xxxxx' => array(),
+ 'enim minim' => array(5, 6),
+ 'enim xxxxx' => array(),
+ 'dolore eu' => array(7),
+ 'dolore xx' => array(),
+ 'ut minim' => array(5),
+ 'xx minim' => array(),
+ 'enim veniam am minim ut' => array(5),
+ // Simple OR queries.
+ 'dolore OR ipsum' => array(1, 2, 7),
+ 'dolore OR xxxxx' => array(2, 7),
+ 'dolore OR ipsum OR enim' => array(1, 2, 4, 5, 6, 7),
+ 'ipsum OR dolore sit OR cillum' => array(2, 7),
+ 'minim dolore OR ipsum' => array(7),
+ 'dolore OR ipsum veniam' => array(7),
+ 'minim dolore OR ipsum OR enim' => array(5, 6, 7),
+ 'dolore xx OR yy' => array(),
+ 'xxxxx dolore OR ipsum' => array(),
+ // Negative queries.
+ 'dolore -sit' => array(7),
+ 'dolore -eu' => array(2),
+ 'dolore -xxxxx' => array(2, 7),
+ 'dolore -xx' => array(2, 7),
+ // Phrase queries.
+ '"dolore sit"' => array(2),
+ '"sit dolore"' => array(),
+ '"am minim veniam es"' => array(6, 7),
+ '"minim am veniam es"' => array(),
+ // Mixed queries.
+ '"am minim veniam es" OR dolore' => array(2, 6, 7),
+ '"minim am veniam es" OR "dolore sit"' => array(2),
+ '"minim am veniam es" OR "sit dolore"' => array(),
+ '"am minim veniam es" -eu' => array(6),
+ '"am minim veniam" -"cillum dolore"' => array(5, 6),
+ '"am minim veniam" -"dolore cillum"' => array(5, 6, 7),
+ 'xxxxx "minim am veniam es" OR dolore' => array(),
+ 'xx "minim am veniam es" OR dolore' => array()
+ );
+ foreach ($queries as $query => $results) {
+ $result = db_select('search_index', 'i')
+ ->extend('SearchQuery')
+ ->searchExpression($query, SEARCH_TYPE)
+ ->execute();
+
+ $set = $result ? $result->fetchAll() : array();
+ $this->_testQueryMatching($query, $set, $results);
+ $this->_testQueryScores($query, $set, $results);
+ }
+
+ // These queries are run against the second index type, SEARCH_TYPE_2.
+ $queries = array(
+ // Simple AND queries.
+ 'ipsum' => array(),
+ 'enim' => array(),
+ 'enim minim' => array(),
+ 'dear' => array(8),
+ 'germany' => array(11, 12),
+ );
+ foreach ($queries as $query => $results) {
+ $result = db_select('search_index', 'i')
+ ->extend('SearchQuery')
+ ->searchExpression($query, SEARCH_TYPE_2)
+ ->execute();
+
+ $set = $result ? $result->fetchAll() : array();
+ $this->_testQueryMatching($query, $set, $results);
+ $this->_testQueryScores($query, $set, $results);
+ }
+
+ // These queries are run against the third index type, SEARCH_TYPE_JPN.
+ $queries = array(
+ // Simple AND queries.
+ '呂波耳' => array(13),
+ '以呂波耳' => array(13),
+ 'ほへと ヌルヲ' => array(13),
+ 'とちリ' => array(),
+ 'ドルーパル' => array(14),
+ 'パルが大' => array(14),
+ 'コーヒー' => array(15),
+ 'ヒーキ' => array(),
+ );
+ foreach ($queries as $query => $results) {
+ $result = db_select('search_index', 'i')
+ ->extend('SearchQuery')
+ ->searchExpression($query, SEARCH_TYPE_JPN)
+ ->execute();
+
+ $set = $result ? $result->fetchAll() : array();
+ $this->_testQueryMatching($query, $set, $results);
+ $this->_testQueryScores($query, $set, $results);
+ }
+ }
+
+ /**
+ * Test the matching abilities of the engine.
+ *
+ * Verify if a query produces the correct results.
+ */
+ function _testQueryMatching($query, $set, $results) {
+ // Get result IDs.
+ $found = array();
+ foreach ($set as $item) {
+ $found[] = $item->sid;
+ }
+
+ // Compare $results and $found.
+ sort($found);
+ sort($results);
+ $this->assertEqual($found, $results, "Query matching '$query'");
+ }
+
+ /**
+ * Test the scoring abilities of the engine.
+ *
+ * Verify if a query produces normalized, monotonous scores.
+ */
+ function _testQueryScores($query, $set, $results) {
+ // Get result scores.
+ $scores = array();
+ foreach ($set as $item) {
+ $scores[] = $item->calculated_score;
+ }
+
+ // Check order.
+ $sorted = $scores;
+ sort($sorted);
+ $this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
+
+ // Check range.
+ $this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
+ }
+}
+
+/**
+ * Tests the bike shed text on no results page, and text on the search page.
+ */
+class SearchPageText extends DrupalWebTestCase {
+ protected $searching_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search page text',
+ 'description' => 'Tests the bike shed text on the no results page, and various other text on search pages.',
+ 'group' => 'Search'
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ // Create user.
+ $this->searching_user = $this->drupalCreateUser(array('search content', 'access user profiles'));
+ }
+
+ /**
+ * Tests the failed search text, and various other text on the search page.
+ */
+ function testSearchText() {
+ $this->drupalLogin($this->searching_user);
+ $this->drupalGet('search/node');
+ $this->assertText(t('Enter your keywords'));
+ $this->assertText(t('Search'));
+ $title = t('Search') . ' | Drupal';
+ $this->assertTitle($title, 'Search page title is correct');
+
+ $edit = array();
+ $edit['keys'] = 'bike shed ' . $this->randomName();
+ $this->drupalPost('search/node', $edit, t('Search'));
+ $this->assertText(t('Consider loosening your query with OR. bike OR shed will often show more results than bike shed.'), 'Help text is displayed when search returns no results.');
+ $this->assertText(t('Search'));
+ $this->assertTitle($title, 'Search page title is correct');
+
+ $edit['keys'] = $this->searching_user->name;
+ $this->drupalPost('search/user', $edit, t('Search'));
+ $this->assertText(t('Search'));
+ $this->assertTitle($title, 'Search page title is correct');
+
+ // Test that search keywords containing slashes are correctly loaded
+ // from the path and displayed in the search form.
+ $arg = $this->randomName() . '/' . $this->randomName();
+ $this->drupalGet('search/node/' . $arg);
+ $input = $this->xpath("//input[@id='edit-keys' and @value='{$arg}']");
+ $this->assertFalse(empty($input), 'Search keys with a / are correctly set as the default value in the search box.');
+
+ // Test a search input exceeding the limit of AND/OR combinations to test
+ // the Denial-of-Service protection.
+ $limit = variable_get('search_and_or_limit', 7);
+ $keys = array();
+ for ($i = 0; $i < $limit + 1; $i++) {
+ $keys[] = $this->randomName(3);
+ if ($i % 2 == 0) {
+ $keys[] = 'OR';
+ }
+ }
+ $edit['keys'] = implode(' ', $keys);
+ $this->drupalPost('search/node', $edit, t('Search'));
+ $this->assertRaw(t('Your search used too many AND/OR expressions. Only the first @count terms were included in this search.', array('@count' => $limit)));
+ }
+}
+
+class SearchAdvancedSearchForm extends DrupalWebTestCase {
+ protected $node;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Advanced search form',
+ 'description' => 'Indexes content and tests the advanced search form.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+ // Create and login user.
+ $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes'));
+ $this->drupalLogin($test_user);
+
+ // Create initial node.
+ $node = $this->drupalCreateNode();
+ $this->node = $this->drupalCreateNode();
+
+ // First update the index. This does the initial processing.
+ node_update_index();
+
+ // Then, run the shutdown function. Testing is a unique case where indexing
+ // and searching has to happen in the same request, so running the shutdown
+ // function manually is needed to finish the indexing process.
+ search_update_totals();
+ }
+
+ /**
+ * Test using the search form with GET and POST queries.
+ * Test using the advanced search form to limit search to nodes of type "Basic page".
+ */
+ function testNodeType() {
+ $this->assertTrue($this->node->type == 'page', 'Node type is Basic page.');
+
+ // Assert that the dummy title doesn't equal the real title.
+ $dummy_title = 'Lorem ipsum';
+ $this->assertNotEqual($dummy_title, $this->node->title, "Dummy title doesn't equal node title");
+
+ // Search for the dummy title with a GET query.
+ $this->drupalGet('search/node/' . $dummy_title);
+ $this->assertNoText($this->node->title, 'Basic page node is not found with dummy title.');
+
+ // Search for the title of the node with a GET query.
+ $this->drupalGet('search/node/' . $this->node->title);
+ $this->assertText($this->node->title, 'Basic page node is found with GET query.');
+
+ // Search for the title of the node with a POST query.
+ $edit = array('or' => $this->node->title);
+ $this->drupalPost('search/node', $edit, t('Advanced search'));
+ $this->assertText($this->node->title, 'Basic page node is found with POST query.');
+
+ // Advanced search type option.
+ $this->drupalPost('search/node', array_merge($edit, array('type[page]' => 'page')), t('Advanced search'));
+ $this->assertText($this->node->title, 'Basic page node is found with POST query and type:page.');
+
+ $this->drupalPost('search/node', array_merge($edit, array('type[article]' => 'article')), t('Advanced search'));
+ $this->assertText('bike shed', 'Article node is not found with POST query and type:article.');
+ }
+}
+
+class SearchRankingTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search engine ranking',
+ 'description' => 'Indexes content and tests ranking factors.',
+ 'group' => 'Search',
+ );
+ }
+
+ /**
+ * Implementation setUp().
+ */
+ function setUp() {
+ parent::setUp('search', 'statistics', 'comment');
+ }
+
+ function testRankings() {
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));
+
+ // Build a list of the rankings to test.
+ $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
+
+ // Create nodes for testing.
+ foreach ($node_ranks as $node_rank) {
+ $settings = array(
+ 'type' => 'page',
+ 'title' => 'Drupal rocks',
+ 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
+ );
+ foreach (array(0, 1) as $num) {
+ if ($num == 1) {
+ switch ($node_rank) {
+ case 'sticky':
+ case 'promote':
+ $settings[$node_rank] = 1;
+ break;
+ case 'relevance':
+ $settings['body'][LANGUAGE_NONE][0]['value'] .= " really rocks";
+ break;
+ case 'recent':
+ $settings['created'] = REQUEST_TIME + 3600;
+ break;
+ case 'comments':
+ $settings['comment'] = 2;
+ break;
+ }
+ }
+ $nodes[$node_rank][$num] = $this->drupalCreateNode($settings);
+ }
+ }
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ // Add a comment to one of the nodes.
+ $edit = array();
+ $edit['subject'] = 'my comment title';
+ $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = 'some random comment';
+ $this->drupalGet('comment/reply/' . $nodes['comments'][1]->nid);
+ $this->drupalPost(NULL, $edit, t('Preview'));
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Enable counting of statistics.
+ variable_set('statistics_count_content_views', 1);
+
+ // Then View one of the nodes a bunch of times.
+ for ($i = 0; $i < 5; $i ++) {
+ $this->drupalGet('node/' . $nodes['views'][1]->nid);
+ }
+
+ // Test each of the possible rankings.
+ foreach ($node_ranks as $node_rank) {
+ // Disable all relevancy rankings except the one we are testing.
+ foreach ($node_ranks as $var) {
+ variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0);
+ }
+
+ // Do the search and assert the results.
+ $set = node_search_execute('rocks');
+ $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
+ }
+ }
+
+ /**
+ * Test rankings of HTML tags.
+ */
+ function testHTMLRankings() {
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->drupalCreateUser(array('create page content')));
+
+ // Test HTML tags with different weights.
+ $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag');
+ $shuffled_tags = $sorted_tags;
+
+ // Shuffle tags to ensure HTML tags are ranked properly.
+ shuffle($shuffled_tags);
+ $settings = array(
+ 'type' => 'page',
+ 'title' => 'Simple node',
+ );
+ foreach ($shuffled_tags as $tag) {
+ switch ($tag) {
+ case 'a':
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => l('Drupal Rocks', 'node'), 'format' => 'full_html')));
+ break;
+ case 'notag':
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'Drupal Rocks')));
+ break;
+ default:
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
+ break;
+ }
+ $nodes[$tag] = $this->drupalCreateNode($settings);
+ }
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ // Disable all other rankings.
+ $node_ranks = array('sticky', 'promote', 'recent', 'comments', 'views');
+ foreach ($node_ranks as $node_rank) {
+ variable_set('node_rank_' . $node_rank, 0);
+ }
+ $set = node_search_execute('rocks');
+
+ // Test the ranking of each tag.
+ foreach ($sorted_tags as $tag_rank => $tag) {
+ // Assert the results.
+ if ($tag == 'notag') {
+ $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for plain text order.');
+ } else {
+ $this->assertEqual($set[$tag_rank]['node']->nid, $nodes[$tag]->nid, 'Search tag ranking for "&lt;' . $sorted_tags[$tag_rank] . '&gt;" order.');
+ }
+ }
+
+ // Test tags with the same weight against the sorted tags.
+ $unsorted_tags = array('u', 'b', 'i', 'strong', 'em');
+ foreach ($unsorted_tags as $tag) {
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => "<$tag>Drupal Rocks</$tag>", 'format' => 'full_html')));
+ $node = $this->drupalCreateNode($settings);
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ $set = node_search_execute('rocks');
+
+ // Ranking should always be second to last.
+ $set = array_slice($set, -2, 1);
+
+ // Assert the results.
+ $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search tag ranking for "&lt;' . $tag . '&gt;" order.');
+
+ // Delete node so it doesn't show up in subsequent search results.
+ node_delete($node->nid);
+ }
+ }
+
+ /**
+ * Verifies that if we combine two rankings, search still works.
+ *
+ * See issue http://drupal.org/node/771596
+ */
+ function testDoubleRankings() {
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content')));
+
+ // See testRankings() above - build a node that will rank high for sticky.
+ $settings = array(
+ 'type' => 'page',
+ 'title' => 'Drupal rocks',
+ 'body' => array(LANGUAGE_NONE => array(array('value' => "Drupal's search rocks"))),
+ 'sticky' => 1,
+ );
+
+ $node = $this->drupalCreateNode($settings);
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ // Set up for ranking sticky and lots of comments; make sure others are
+ // disabled.
+ $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views');
+ foreach ($node_ranks as $var) {
+ $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0;
+ variable_set('node_rank_' . $var, $value);
+ }
+
+ // Do the search and assert the results.
+ $set = node_search_execute('rocks');
+ $this->assertEqual($set[0]['node']->nid, $node->nid, 'Search double ranking order.');
+ }
+}
+
+class SearchBlockTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Block availability',
+ 'description' => 'Check if the search form block is available.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ // Create and login user
+ $admin_user = $this->drupalCreateUser(array('administer blocks', 'search content'));
+ $this->drupalLogin($admin_user);
+ }
+
+ function testSearchFormBlock() {
+ // Set block title to confirm that the interface is available.
+ $this->drupalPost('admin/structure/block/manage/search/form/configure', array('title' => $this->randomName(8)), t('Save block'));
+ $this->assertText(t('The block configuration has been saved.'), 'Block configuration set.');
+
+ // Set the block to a region to confirm block is available.
+ $edit = array();
+ $edit['blocks[search_form][region]'] = 'footer';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ $this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.');
+ }
+
+ /**
+ * Test that the search block form works correctly.
+ */
+ function testBlock() {
+ // Enable the block, and place it in the 'content' region so that it isn't
+ // hidden on 404 pages.
+ $edit = array('blocks[search_form][region]' => 'content');
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+
+ // Test a normal search via the block form, from the front page.
+ $terms = array('search_block_form' => 'test');
+ $this->drupalPost('node', $terms, t('Search'));
+ $this->assertText('Your search yielded no results');
+
+ // Test a search from the block on a 404 page.
+ $this->drupalGet('foo');
+ $this->assertResponse(404);
+ $this->drupalPost(NULL, $terms, t('Search'));
+ $this->assertResponse(200);
+ $this->assertText('Your search yielded no results');
+
+ // Test a search from the block when it doesn't appear on the search page.
+ $edit = array('pages' => 'search');
+ $this->drupalPost('admin/structure/block/manage/search/form/configure', $edit, t('Save block'));
+ $this->drupalPost('node', $terms, t('Search'));
+ $this->assertText('Your search yielded no results');
+
+ // Confirm that the user is redirected to the search page.
+ $this->assertEqual(
+ $this->getUrl(),
+ url('search/node/' . $terms['search_block_form'], array('absolute' => TRUE)),
+ 'Redirected to correct url.'
+ );
+
+ // Test an empty search via the block form, from the front page.
+ $terms = array('search_block_form' => '');
+ $this->drupalPost('node', $terms, t('Search'));
+ $this->assertText('Please enter some keywords');
+
+ // Confirm that the user is redirected to the search page, when form is submitted empty.
+ $this->assertEqual(
+ $this->getUrl(),
+ url('search/node/', array('absolute' => TRUE)),
+ 'Redirected to correct url.'
+ );
+ }
+}
+
+/**
+ * Tests that searching for a phrase gets the correct page count.
+ */
+class SearchExactTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search engine phrase queries',
+ 'description' => 'Tests that searching for a phrase gets the correct page count.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+ }
+
+ /**
+ * Tests that the correct number of pager links are found for both keywords and phrases.
+ */
+ function testExactQuery() {
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->drupalCreateUser(array('create page content', 'search content')));
+
+ $settings = array(
+ 'type' => 'page',
+ 'title' => 'Simple Node',
+ );
+ // Create nodes with exact phrase.
+ for ($i = 0; $i <= 17; $i++) {
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love pizza')));
+ $this->drupalCreateNode($settings);
+ }
+ // Create nodes containing keywords.
+ for ($i = 0; $i <= 17; $i++) {
+ $settings['body'] = array(LANGUAGE_NONE => array(array('value' => 'love cheesy pizza')));
+ $this->drupalCreateNode($settings);
+ }
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ // Test that the correct number of pager links are found for keyword search.
+ $edit = array('keys' => 'love pizza');
+ $this->drupalPost('search/node', $edit, t('Search'));
+ $this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
+ $this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
+ $this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
+ $this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
+
+ // Test that the correct number of pager links are found for exact phrase search.
+ $edit = array('keys' => '"love pizza"');
+ $this->drupalPost('search/node', $edit, t('Search'));
+ $this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
+ $this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
+ }
+}
+
+/**
+ * Test integration searching comments.
+ */
+class SearchCommentTestCase extends DrupalWebTestCase {
+ protected $admin_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Comment Search tests',
+ 'description' => 'Verify text formats and filters used elsewhere.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('comment', 'search');
+
+ // Create and log in an administrative user having access to the Full HTML
+ // text format.
+ $full_html_format = filter_format_load('full_html');
+ $permissions = array(
+ 'administer filters',
+ filter_permission_name($full_html_format),
+ 'administer permissions',
+ 'create page content',
+ 'skip comment approval',
+ 'access comments',
+ );
+ $this->admin_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->admin_user);
+ }
+
+ /**
+ * Verify that comments are rendered using proper format in search results.
+ */
+ function testSearchResultsComment() {
+ $comment_body = 'Test comment body';
+
+ variable_set('comment_preview_article', DRUPAL_OPTIONAL);
+ // Enable check_plain() for 'Filtered HTML' text format.
+ $filtered_html_format_id = 'filtered_html';
+ $edit = array(
+ 'filters[filter_html_escape][status]' => TRUE,
+ );
+ $this->drupalPost('admin/config/content/formats/' . $filtered_html_format_id, $edit, t('Save configuration'));
+ // Allow anonymous users to search content.
+ $edit = array(
+ DRUPAL_ANONYMOUS_RID . '[search content]' => 1,
+ DRUPAL_ANONYMOUS_RID . '[access comments]' => 1,
+ DRUPAL_ANONYMOUS_RID . '[post comments]' => 1,
+ );
+ $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
+
+ // Create a node.
+ $node = $this->drupalCreateNode(array('type' => 'article'));
+ // Post a comment using 'Full HTML' text format.
+ $edit_comment = array();
+ $edit_comment['subject'] = 'Test comment subject';
+ $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
+ $full_html_format_id = 'full_html';
+ $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $full_html_format_id;
+ $this->drupalPost('comment/reply/' . $node->nid, $edit_comment, t('Save'));
+
+ // Invoke search index update.
+ $this->drupalLogout();
+ $this->cronRun();
+
+ // Search for the comment subject.
+ $edit = array(
+ 'search_block_form' => "'" . $edit_comment['subject'] . "'",
+ );
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertText($node->title, 'Node found in search results.');
+ $this->assertText($edit_comment['subject'], 'Comment subject found in search results.');
+
+ // Search for the comment body.
+ $edit = array(
+ 'search_block_form' => "'" . $comment_body . "'",
+ );
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertText($node->title, 'Node found in search results.');
+
+ // Verify that comment is rendered using proper format.
+ $this->assertText($comment_body, 'Comment body text found in search results.');
+ $this->assertNoRaw(t('n/a'), 'HTML in comment body is not hidden.');
+ $this->assertNoRaw(check_plain($edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]']), 'HTML in comment body is not escaped.');
+
+ // Hide comments.
+ $this->drupalLogin($this->admin_user);
+ $node->comment = 0;
+ node_save($node);
+
+ // Invoke search index update.
+ $this->drupalLogout();
+ $this->cronRun();
+
+ // Search for $title.
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertNoText($comment_body, 'Comment body text not found in search results.');
+ }
+
+ /**
+ * Verify access rules for comment indexing with different permissions.
+ */
+ function testSearchResultsCommentAccess() {
+ $comment_body = 'Test comment body';
+ $this->comment_subject = 'Test comment subject';
+ $this->admin_role = $this->admin_user->roles;
+ unset($this->admin_role[DRUPAL_AUTHENTICATED_RID]);
+ $this->admin_role = key($this->admin_role);
+
+ // Create a node.
+ variable_set('comment_preview_article', DRUPAL_OPTIONAL);
+ $this->node = $this->drupalCreateNode(array('type' => 'article'));
+
+ // Post a comment using 'Full HTML' text format.
+ $edit_comment = array();
+ $edit_comment['subject'] = $this->comment_subject;
+ $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = '<h1>' . $comment_body . '</h1>';
+ $this->drupalPost('comment/reply/' . $this->node->nid, $edit_comment, t('Save'));
+
+ $this->drupalLogout();
+ $this->setRolePermissions(DRUPAL_ANONYMOUS_RID);
+ $this->checkCommentAccess('Anon user has search permission but no access comments permission, comments should not be indexed');
+
+ $this->setRolePermissions(DRUPAL_ANONYMOUS_RID, TRUE);
+ $this->checkCommentAccess('Anon user has search permission and access comments permission, comments should be indexed', TRUE);
+
+ $this->drupalLogin($this->admin_user);
+ $this->drupalGet('admin/people/permissions');
+
+ // Disable search access for authenticated user to test admin user.
+ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, FALSE, FALSE);
+
+ $this->setRolePermissions($this->admin_role);
+ $this->checkCommentAccess('Admin user has search permission but no access comments permission, comments should not be indexed');
+
+ $this->setRolePermissions($this->admin_role, TRUE);
+ $this->checkCommentAccess('Admin user has search permission and access comments permission, comments should be indexed', TRUE);
+
+ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID);
+ $this->checkCommentAccess('Authenticated user has search permission but no access comments permission, comments should not be indexed');
+
+ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE);
+ $this->checkCommentAccess('Authenticated user has search permission and access comments permission, comments should be indexed', TRUE);
+
+ // Verify that access comments permission is inherited from the
+ // authenticated role.
+ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, FALSE);
+ $this->setRolePermissions($this->admin_role);
+ $this->checkCommentAccess('Admin user has search permission and no access comments permission, but comments should be indexed because admin user inherits authenticated user\'s permission to access comments', TRUE);
+
+ // Verify that search content permission is inherited from the authenticated
+ // role.
+ $this->setRolePermissions(DRUPAL_AUTHENTICATED_RID, TRUE, TRUE);
+ $this->setRolePermissions($this->admin_role, TRUE, FALSE);
+ $this->checkCommentAccess('Admin user has access comments permission and no search permission, but comments should be indexed because admin user inherits authenticated user\'s permission to search', TRUE);
+
+ }
+
+ /**
+ * Set permissions for role.
+ */
+ function setRolePermissions($rid, $access_comments = FALSE, $search_content = TRUE) {
+ $permissions = array(
+ 'access comments' => $access_comments,
+ 'search content' => $search_content,
+ );
+ user_role_change_permissions($rid, $permissions);
+ }
+
+ /**
+ * Update search index and search for comment.
+ */
+ function checkCommentAccess($message, $assume_access = FALSE) {
+ // Invoke search index update.
+ search_touch_node($this->node->nid);
+ $this->cronRun();
+
+ // Search for the comment subject.
+ $edit = array(
+ 'search_block_form' => "'" . $this->comment_subject . "'",
+ );
+ $this->drupalPost('', $edit, t('Search'));
+ $method = $assume_access ? 'assertText' : 'assertNoText';
+ $verb = $assume_access ? 'found' : 'not found';
+ $this->{$method}($this->node->title, "Node $verb in search results: " . $message);
+ $this->{$method}($this->comment_subject, "Comment subject $verb in search results: " . $message);
+ }
+
+ /**
+ * Verify that 'add new comment' does not appear in search results or index.
+ */
+ function testAddNewComment() {
+ // Create a node with a short body.
+ $settings = array(
+ 'type' => 'article',
+ 'title' => 'short title',
+ 'body' => array(LANGUAGE_NONE => array(array('value' => 'short body text'))),
+ );
+
+ $user = $this->drupalCreateUser(array('search content', 'create article content', 'access content'));
+ $this->drupalLogin($user);
+
+ $node = $this->drupalCreateNode($settings);
+ // Verify that if you view the node on its own page, 'add new comment'
+ // is there.
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertText(t('Add new comment'), 'Add new comment appears on node page');
+
+ // Run cron to index this page.
+ $this->drupalLogout();
+ $this->cronRun();
+
+ // Search for 'comment'. Should be no results.
+ $this->drupalLogin($user);
+ $this->drupalPost('search/node', array('keys' => 'comment'), t('Search'));
+ $this->assertText(t('Your search yielded no results'), 'No results searching for the word comment');
+
+ // Search for the node title. Should be found, and 'Add new comment' should
+ // not be part of the search snippet.
+ $this->drupalPost('search/node', array('keys' => 'short'), t('Search'));
+ $this->assertText($node->title, 'Search for keyword worked');
+ $this->assertNoText(t('Add new comment'), 'Add new comment does not appear on search results page');
+ }
+
+}
+
+/**
+ * Tests search_expression_insert() and search_expression_extract().
+ *
+ * @see http://drupal.org/node/419388 (issue)
+ */
+class SearchExpressionInsertExtractTestCase extends DrupalUnitTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search expression insert/extract',
+ 'description' => 'Tests the functions search_expression_insert() and search_expression_extract()',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ drupal_load('module', 'search');
+ parent::setUp();
+ }
+
+ /**
+ * Tests search_expression_insert() and search_expression_extract().
+ */
+ function testInsertExtract() {
+ $base_expression = "mykeyword";
+ // Build an array of option, value, what should be in the expression, what
+ // should be retrieved from expression.
+ $cases = array(
+ array('foo', 'bar', 'foo:bar', 'bar'), // Normal case.
+ array('foo', NULL, '', NULL), // Empty value: shouldn't insert.
+ array('foo', ' ', 'foo:', ''), // Space as value: should insert but retrieve empty string.
+ array('foo', '', 'foo:', ''), // Empty string as value: should insert but retrieve empty string.
+ array('foo', '0', 'foo:0', '0'), // String zero as value: should insert.
+ array('foo', 0, 'foo:0', '0'), // Numeric zero as value: should insert.
+ );
+
+ foreach ($cases as $index => $case) {
+ $after_insert = search_expression_insert($base_expression, $case[0], $case[1]);
+ if (empty($case[2])) {
+ $this->assertEqual($after_insert, $base_expression, "Empty insert does not change expression in case $index");
+ }
+ else {
+ $this->assertEqual($after_insert, $base_expression . ' ' . $case[2], "Insert added correct expression for case $index");
+ }
+
+ $retrieved = search_expression_extract($after_insert, $case[0]);
+ if (!isset($case[3])) {
+ $this->assertFalse(isset($retrieved), "Empty retrieval results in unset value in case $index");
+ }
+ else {
+ $this->assertEqual($retrieved, $case[3], "Value is retrieved for case $index");
+ }
+
+ $after_clear = search_expression_insert($after_insert, $case[0]);
+ $this->assertEqual(trim($after_clear), $base_expression, "After clearing, base expression is restored for case $index");
+
+ $cleared = search_expression_extract($after_clear, $case[0]);
+ $this->assertFalse(isset($cleared), "After clearing, value could not be retrieved for case $index");
+ }
+ }
+}
+
+/**
+ * Tests that comment count display toggles properly on comment status of node
+ *
+ * Issue 537278
+ *
+ * - Nodes with comment status set to Open should always how comment counts
+ * - Nodes with comment status set to Closed should show comment counts
+ * only when there are comments
+ * - Nodes with comment status set to Hidden should never show comment counts
+ */
+class SearchCommentCountToggleTestCase extends DrupalWebTestCase {
+ protected $searching_user;
+ protected $searchable_nodes;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Comment count toggle',
+ 'description' => 'Verify that comment count display toggles properly on comment status of node.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ // Create searching user.
+ $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
+
+ // Create initial nodes.
+ $node_params = array('type' => 'article', 'body' => array(LANGUAGE_NONE => array(array('value' => 'SearchCommentToggleTestCase'))));
+
+ $this->searchable_nodes['1 comment'] = $this->drupalCreateNode($node_params);
+ $this->searchable_nodes['0 comments'] = $this->drupalCreateNode($node_params);
+
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->searching_user);
+
+ // Create a comment array
+ $edit_comment = array();
+ $edit_comment['subject'] = $this->randomName();
+ $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
+ $filtered_html_format_id = 'filtered_html';
+ $edit_comment['comment_body[' . LANGUAGE_NONE . '][0][format]'] = $filtered_html_format_id;
+
+ // Post comment to the test node with comment
+ $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->nid, $edit_comment, t('Save'));
+
+ // First update the index. This does the initial processing.
+ node_update_index();
+
+ // Then, run the shutdown function. Testing is a unique case where indexing
+ // and searching has to happen in the same request, so running the shutdown
+ // function manually is needed to finish the indexing process.
+ search_update_totals();
+ }
+
+ /**
+ * Verify that comment count display toggles properly on comment status of node
+ */
+ function testSearchCommentCountToggle() {
+ // Search for the nodes by string in the node body.
+ $edit = array(
+ 'search_block_form' => "'SearchCommentToggleTestCase'",
+ );
+
+ // Test comment count display for nodes with comment status set to Open
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
+ $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');
+
+ // Test comment count display for nodes with comment status set to Closed
+ $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_CLOSED;
+ node_save($this->searchable_nodes['0 comments']);
+ $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_CLOSED;
+ node_save($this->searchable_nodes['1 comment']);
+
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
+ $this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');
+
+ // Test comment count display for nodes with comment status set to Hidden
+ $this->searchable_nodes['0 comments']->comment = COMMENT_NODE_HIDDEN;
+ node_save($this->searchable_nodes['0 comments']);
+ $this->searchable_nodes['1 comment']->comment = COMMENT_NODE_HIDDEN;
+ node_save($this->searchable_nodes['1 comment']);
+
+ $this->drupalPost('', $edit, t('Search'));
+ $this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
+ $this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
+ }
+}
+
+/**
+ * Test search_simplify() on every Unicode character, and some other cases.
+ */
+class SearchSimplifyTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search simplify',
+ 'description' => 'Check that the search_simply() function works as intended.',
+ 'group' => 'Search',
+ );
+ }
+
+ /**
+ * Tests that all Unicode characters simplify correctly.
+ */
+ function testSearchSimplifyUnicode() {
+ // This test uses a file that was constructed so that the even lines are
+ // boundary characters, and the odd lines are valid word characters. (It
+ // was generated as a sequence of all the Unicode characters, and then the
+ // boundary chararacters (punctuation, spaces, etc.) were split off into
+ // their own lines). So the even-numbered lines should simplify to nothing,
+ // and the odd-numbered lines we need to split into shorter chunks and
+ // verify that simplification doesn't lose any characters.
+ $input = file_get_contents(DRUPAL_ROOT . '/modules/search/tests/UnicodeTest.txt');
+ $basestrings = explode(chr(10), $input);
+ $strings = array();
+ foreach ($basestrings as $key => $string) {
+ if ($key %2) {
+ // Even line - should simplify down to a space.
+ $simplified = search_simplify($string);
+ $this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
+ }
+ else {
+ // Odd line, should be word characters.
+ // Split this into 30-character chunks, so we don't run into limits
+ // of truncation in search_simplify().
+ $start = 0;
+ while ($start < drupal_strlen($string)) {
+ $newstr = drupal_substr($string, $start, 30);
+ // Special case: leading zeros are removed from numeric strings,
+ // and there's one string in this file that is numbers starting with
+ // zero, so prepend a 1 on that string.
+ if (preg_match('/^[0-9]+$/', $newstr)) {
+ $newstr = '1' . $newstr;
+ }
+ $strings[] = $newstr;
+ $start += 30;
+ }
+ }
+ }
+ foreach ($strings as $key => $string) {
+ $simplified = search_simplify($string);
+ $this->assertTrue(drupal_strlen($simplified) >= drupal_strlen($string), "Nothing is removed from string $key.");
+ }
+
+ // Test the low-numbered ASCII control characters separately. They are not
+ // in the text file because they are problematic for diff, especially \0.
+ $string = '';
+ for ($i = 0; $i < 32; $i++) {
+ $string .= chr($i);
+ }
+ $this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
+ }
+
+ /**
+ * Tests that search_simplify() does the right thing with punctuation.
+ */
+ function testSearchSimplifyPunctuation() {
+ $cases = array(
+ array('20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'),
+ array('great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'),
+ array('very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'),
+ array('regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'),
+ );
+
+ foreach ($cases as $case) {
+ $out = trim(search_simplify($case[0]));
+ $this->assertEqual($out, $case[1], $case[2]);
+ }
+ }
+}
+
+
+/**
+ * Tests keywords and conditions.
+ */
+class SearchKeywordsConditions extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Keywords and conditions',
+ 'description' => 'Verify the search pulls in keywords and extra conditions.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search', 'search_extra_type');
+ // Create searching user.
+ $this->searching_user = $this->drupalCreateUser(array('search content', 'access content', 'access comments', 'skip comment approval'));
+ // Login with sufficient privileges.
+ $this->drupalLogin($this->searching_user);
+ // Test with all search modules enabled.
+ variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
+ menu_rebuild();
+ }
+
+ /**
+ * Verify the kewords are captured and conditions respected.
+ */
+ function testSearchKeyswordsConditions() {
+ // No keys, not conditions - no results.
+ $this->drupalGet('search/dummy_path');
+ $this->assertNoText('Dummy search snippet to display');
+ // With keys - get results.
+ $keys = 'bike shed ' . $this->randomName();
+ $this->drupalGet("search/dummy_path/{$keys}");
+ $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
+ $keys = 'blue drop ' . $this->randomName();
+ $this->drupalGet("search/dummy_path", array('query' => array('keys' => $keys)));
+ $this->assertText("Dummy search snippet to display. Keywords: {$keys}");
+ // Add some conditions and keys.
+ $keys = 'moving drop ' . $this->randomName();
+ $this->drupalGet("search/dummy_path/bike", array('query' => array('search_conditions' => $keys)));
+ $this->assertText("Dummy search snippet to display.");
+ $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
+ // Add some conditions and no keys.
+ $keys = 'drop kick ' . $this->randomName();
+ $this->drupalGet("search/dummy_path", array('query' => array('search_conditions' => $keys)));
+ $this->assertText("Dummy search snippet to display.");
+ $this->assertRaw(print_r(array('search_conditions' => $keys), TRUE));
+ }
+}
+
+/**
+ * Tests that numbers can be searched.
+ */
+class SearchNumbersTestCase extends DrupalWebTestCase {
+ protected $test_user;
+ protected $numbers;
+ protected $nodes;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search numbers',
+ 'description' => 'Check that numbers can be searched',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
+ $this->drupalLogin($this->test_user);
+
+ // Create content with various numbers in it.
+ // Note: 50 characters is the current limit of the search index's word
+ // field.
+ $this->numbers = array(
+ 'ISBN' => '978-0446365383',
+ 'UPC' => '036000 291452',
+ 'EAN bar code' => '5901234123457',
+ 'negative' => '-123456.7890',
+ 'quoted negative' => '"-123456.7890"',
+ 'leading zero' => '0777777777',
+ 'tiny' => '111',
+ 'small' => '22222222222222',
+ 'medium' => '333333333333333333333333333',
+ 'large' => '444444444444444444444444444444444444444',
+ 'gigantic' => '5555555555555555555555555555555555555555555555555',
+ 'over fifty characters' => '666666666666666666666666666666666666666666666666666666666666',
+ 'date', '01/02/2009',
+ 'commas', '987,654,321',
+ );
+
+ foreach ($this->numbers as $doc => $num) {
+ $info = array(
+ 'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
+ 'type' => 'page',
+ 'language' => LANGUAGE_NONE,
+ 'title' => $doc . ' number',
+ );
+ $this->nodes[$doc] = $this->drupalCreateNode($info);
+ }
+
+ // Run cron to ensure the content is indexed.
+ $this->cronRun();
+ $this->drupalGet('admin/reports/dblog');
+ $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
+ }
+
+ /**
+ * Tests that all the numbers can be searched.
+ */
+ function testNumberSearching() {
+ $types = array_keys($this->numbers);
+
+ foreach ($types as $type) {
+ $number = $this->numbers[$type];
+ // If the number is negative, remove the - sign, because - indicates
+ // "not keyword" when searching.
+ $number = ltrim($number, '-');
+ $node = $this->nodes[$type];
+
+ // Verify that the node title does not appear on the search page
+ // with a dummy search.
+ $this->drupalPost('search/node',
+ array('keys' => 'foo'),
+ t('Search'));
+ $this->assertNoText($node->title, $type . ': node title not shown in dummy search');
+
+ // Verify that the node title does appear as a link on the search page
+ // when searching for the number.
+ $this->drupalPost('search/node',
+ array('keys' => $number),
+ t('Search'));
+ $this->assertText($node->title, format_string('%type: node title shown (search found the node) in search for number %number.', array('%type' => $type, '%number' => $number)));
+ }
+ }
+}
+
+/**
+ * Tests that numbers can be searched, with more complex matching.
+ */
+class SearchNumberMatchingTestCase extends DrupalWebTestCase {
+ protected $test_user;
+ protected $numbers;
+ protected $nodes;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search number matching',
+ 'description' => 'Check that numbers can be searched with more complex matching',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ $this->test_user = $this->drupalCreateUser(array('search content', 'access content', 'administer nodes', 'access site reports'));
+ $this->drupalLogin($this->test_user);
+
+ // Define a group of numbers that should all match each other --
+ // numbers with internal punctuation should match each other, as well
+ // as numbers with and without leading zeros and leading/trailing
+ // . and -.
+ $this->numbers = array(
+ '123456789',
+ '12/34/56789',
+ '12.3456789',
+ '12-34-56789',
+ '123,456,789',
+ '-123456789',
+ '0123456789',
+ );
+
+ foreach ($this->numbers as $num) {
+ $info = array(
+ 'body' => array(LANGUAGE_NONE => array(array('value' => $num))),
+ 'type' => 'page',
+ 'language' => LANGUAGE_NONE,
+ );
+ $this->nodes[] = $this->drupalCreateNode($info);
+ }
+
+ // Run cron to ensure the content is indexed.
+ $this->cronRun();
+ $this->drupalGet('admin/reports/dblog');
+ $this->assertText(t('Cron run completed'), 'Log shows cron run completed');
+ }
+
+ /**
+ * Tests that all the numbers can be searched.
+ */
+ function testNumberSearching() {
+ for ($i = 0; $i < count($this->numbers); $i++) {
+ $node = $this->nodes[$i];
+
+ // Verify that the node title does not appear on the search page
+ // with a dummy search.
+ $this->drupalPost('search/node',
+ array('keys' => 'foo'),
+ t('Search'));
+ $this->assertNoText($node->title, format_string('%number: node title not shown in dummy search', array('%number' => $i)));
+
+ // Now verify that we can find node i by searching for any of the
+ // numbers.
+ for ($j = 0; $j < count($this->numbers); $j++) {
+ $number = $this->numbers[$j];
+ // If the number is negative, remove the - sign, because - indicates
+ // "not keyword" when searching.
+ $number = ltrim($number, '-');
+
+ $this->drupalPost('search/node',
+ array('keys' => $number),
+ t('Search'));
+ $this->assertText($node->title, format_string('%i: node title shown (search found the node) in search for number %number', array('%i' => $i, '%number' => $number)));
+ }
+ }
+
+ }
+}
+
+/**
+ * Test config page.
+ */
+class SearchConfigSettingsForm extends DrupalWebTestCase {
+ public $search_user;
+ public $search_node;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Config settings form',
+ 'description' => 'Verify the search config settings form.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search', 'search_extra_type');
+
+ // Login as a user that can create and search content.
+ $this->search_user = $this->drupalCreateUser(array('search content', 'administer search', 'administer nodes', 'bypass node access', 'access user profiles', 'administer users', 'administer blocks'));
+ $this->drupalLogin($this->search_user);
+
+ // Add a single piece of content and index it.
+ $node = $this->drupalCreateNode();
+ $this->search_node = $node;
+ // Link the node to itself to test that it's only indexed once. The content
+ // also needs the word "pizza" so we can use it as the search keyword.
+ $langcode = LANGUAGE_NONE;
+ $body_key = "body[$langcode][0][value]";
+ $edit[$body_key] = l($node->title, 'node/' . $node->nid) . ' pizza sandwich';
+ $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
+
+ node_update_index();
+ search_update_totals();
+
+ // Enable the search block.
+ $edit = array();
+ $edit['blocks[search_form][region]'] = 'content';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ }
+
+ /**
+ * Verify the search settings form.
+ */
+ function testSearchSettingsPage() {
+
+ // Test that the settings form displays the correct count of items left to index.
+ $this->drupalGet('admin/config/search/settings');
+ $this->assertText(t('There are @count items left to index.', array('@count' => 0)));
+
+ // Test the re-index button.
+ $this->drupalPost('admin/config/search/settings', array(), t('Re-index site'));
+ $this->assertText(t('Are you sure you want to re-index the site'));
+ $this->drupalPost('admin/config/search/settings/reindex', array(), t('Re-index site'));
+ $this->assertText(t('The index will be rebuilt'));
+ $this->drupalGet('admin/config/search/settings');
+ $this->assertText(t('There is 1 item left to index.'));
+
+ // Test that the form saves with the default values.
+ $this->drupalPost('admin/config/search/settings', array(), t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved.'), 'Form saves with the default values.');
+
+ // Test that the form does not save with an invalid word length.
+ $edit = array(
+ 'minimum_word_size' => $this->randomName(3),
+ );
+ $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
+ $this->assertNoText(t('The configuration options have been saved.'), 'Form does not save with an invalid word length.');
+ }
+
+ /**
+ * Verify that you can disable individual search modules.
+ */
+ function testSearchModuleDisabling() {
+ // Array of search modules to test: 'path' is the search path, 'title' is
+ // the tab title, 'keys' are the keywords to search for, and 'text' is
+ // the text to assert is on the results page.
+ $module_info = array(
+ 'node' => array(
+ 'path' => 'node',
+ 'title' => 'Content',
+ 'keys' => 'pizza',
+ 'text' => $this->search_node->title,
+ ),
+ 'user' => array(
+ 'path' => 'user',
+ 'title' => 'User',
+ 'keys' => $this->search_user->name,
+ 'text' => $this->search_user->mail,
+ ),
+ 'search_extra_type' => array(
+ 'path' => 'dummy_path',
+ 'title' => 'Dummy search type',
+ 'keys' => 'foo',
+ 'text' => 'Dummy search snippet to display',
+ ),
+ );
+ $modules = array_keys($module_info);
+
+ // Test each module if it's enabled as the only search module.
+ foreach ($modules as $module) {
+ // Enable the one module and disable other ones.
+ $info = $module_info[$module];
+ $edit = array();
+ foreach ($modules as $other) {
+ $edit['search_active_modules[' . $other . ']'] = (($other == $module) ? $module : FALSE);
+ }
+ $edit['search_default_module'] = $module;
+ $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
+
+ // Run a search from the correct search URL.
+ $this->drupalGet('search/' . $info['path'] . '/' . $info['keys']);
+ $this->assertNoText('no results', $info['title'] . ' search found results');
+ $this->assertText($info['text'], 'Correct search text found');
+
+ // Verify that other module search tab titles are not visible.
+ foreach ($modules as $other) {
+ if ($other != $module) {
+ $title = $module_info[$other]['title'];
+ $this->assertNoText($title, $title . ' search tab is not shown');
+ }
+ }
+
+ // Run a search from the search block on the node page. Verify you get
+ // to this module's search results page.
+ $terms = array('search_block_form' => $info['keys']);
+ $this->drupalPost('node', $terms, t('Search'));
+ $this->assertEqual(
+ $this->getURL(),
+ url('search/' . $info['path'] . '/' . $info['keys'], array('absolute' => TRUE)),
+ 'Block redirected to right search page');
+
+ // Try an invalid search path. Should redirect to our active module.
+ $this->drupalGet('search/not_a_module_path');
+ $this->assertEqual(
+ $this->getURL(),
+ url('search/' . $info['path'], array('absolute' => TRUE)),
+ 'Invalid search path redirected to default search page');
+ }
+
+ // Test with all search modules enabled. When you go to the search
+ // page or run search, all modules should be shown.
+ $edit = array();
+ foreach ($modules as $module) {
+ $edit['search_active_modules[' . $module . ']'] = $module;
+ }
+ $edit['search_default_module'] = 'node';
+
+ $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration'));
+
+ foreach (array('search/node/pizza', 'search/node') as $path) {
+ $this->drupalGet($path);
+ foreach ($modules as $module) {
+ $title = $module_info[$module]['title'];
+ $this->assertText($title, format_string('%title search tab is shown', array('%title' => $title)));
+ }
+ }
+ }
+}
+
+/**
+ * Tests the search_excerpt() function.
+ */
+class SearchExcerptTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search excerpt extraction',
+ 'description' => 'Tests that the search_excerpt() function works.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+ }
+
+ /**
+ * Tests search_excerpt() with several simulated search keywords.
+ *
+ * Passes keywords and a sample marked up string, "The quick
+ * brown fox jumps over the lazy dog", and compares it to the
+ * correctly marked up string. The correctly marked up string
+ * contains either highlighted keywords or the original marked
+ * up string if no keywords matched the string.
+ */
+ function testSearchExcerpt() {
+ // Make some text with entities and tags.
+ $text = 'The <strong>quick</strong> <a href="#">brown</a> fox &amp; jumps <h2>over</h2> the lazy dog';
+ // Note: The search_excerpt() function adds some extra spaces -- not
+ // important for HTML formatting. Remove these for comparison.
+ $expected = 'The quick brown fox &amp; jumps over the lazy dog';
+ $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
+ $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
+ $this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog ...', 'Found keyword is highlighted');
+
+ $longtext = str_repeat($text . ' ', 10);
+ $result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext));
+ $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
+
+ $entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
+ $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
+ $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
+ $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
+
+ // The node body that will produce this rendered $text is:
+ // 123456789 HTMLTest +123456789+&lsquo; +&lsquo; +&lsquo; +&lsquo; +12345678 &nbsp;&nbsp; +&lsquo; +&lsquo; +&lsquo; &lsquo;
+ $text = "<div class=\"field field-name-body field-type-text-with-summary field-label-hidden\"><div class=\"field-items\"><div class=\"field-item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678    +‘ +‘ +‘ ‘</p>\n</div></div></div> ";
+ $result = search_excerpt('HTMLTest', $text);
+ $this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
+ }
+
+ /**
+ * Tests search_excerpt() with search keywords matching simplified words.
+ *
+ * Excerpting should handle keywords that are matched only after going through
+ * search_simplify(). This test passes keywords that match simplified words
+ * and compares them with strings that contain the original unsimplified word.
+ */
+ function testSearchExcerptSimplified() {
+ $lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.';
+ $lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.';
+
+ // Make some text with some keywords that will get simplified.
+ $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
+ // Note: The search_excerpt() function adds some extra spaces -- not
+ // important for HTML formatting. Remove these for comparison.
+ $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text));
+ $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text));
+ $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text));
+ $this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text));
+ $this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text));
+ $this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text));
+ $this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
+
+ // Test phrases with characters which are being truncated.
+ $result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text));
+ $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text));
+ $this->assertTrue(strpos($result, '<strong>ipsum </strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
+
+ // Test combination of the valid keyword and keyword containing only
+ // characters which are being truncated during simplification.
+ $result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text));
+ $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
+
+ $result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text));
+ $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
+ }
+}
+
+/**
+ * Test the CJK tokenizer.
+ */
+class SearchTokenizerTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'CJK tokenizer',
+ 'description' => 'Check that CJK tokenizer works as intended.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+ }
+
+ /**
+ * Verifies that strings of CJK characters are tokenized.
+ *
+ * The search_simplify() function does special things with numbers, symbols,
+ * and punctuation. So we only test that CJK characters that are not in these
+ * character classes are tokenized properly. See PREG_CLASS_CKJ for more
+ * information.
+ */
+ function testTokenizer() {
+ // Set the minimum word size to 1 (to split all CJK characters) and make
+ // sure CJK tokenizing is turned on.
+ variable_set('minimum_word_size', 1);
+ variable_set('overlap_cjk', TRUE);
+ $this->refreshVariables();
+
+ // Create a string of CJK characters from various character ranges in
+ // the Unicode tables.
+
+ // Beginnings of the character ranges.
+ $starts = array(
+ 'CJK unified' => 0x4e00,
+ 'CJK Ext A' => 0x3400,
+ 'CJK Compat' => 0xf900,
+ 'Hangul Jamo' => 0x1100,
+ 'Hangul Ext A' => 0xa960,
+ 'Hangul Ext B' => 0xd7b0,
+ 'Hangul Compat' => 0x3131,
+ 'Half non-punct 1' => 0xff21,
+ 'Half non-punct 2' => 0xff41,
+ 'Half non-punct 3' => 0xff66,
+ 'Hangul Syllables' => 0xac00,
+ 'Hiragana' => 0x3040,
+ 'Katakana' => 0x30a1,
+ 'Katakana Ext' => 0x31f0,
+ 'CJK Reserve 1' => 0x20000,
+ 'CJK Reserve 2' => 0x30000,
+ 'Bomofo' => 0x3100,
+ 'Bomofo Ext' => 0x31a0,
+ 'Lisu' => 0xa4d0,
+ 'Yi' => 0xa000,
+ );
+
+ // Ends of the character ranges.
+ $ends = array(
+ 'CJK unified' => 0x9fcf,
+ 'CJK Ext A' => 0x4dbf,
+ 'CJK Compat' => 0xfaff,
+ 'Hangul Jamo' => 0x11ff,
+ 'Hangul Ext A' => 0xa97f,
+ 'Hangul Ext B' => 0xd7ff,
+ 'Hangul Compat' => 0x318e,
+ 'Half non-punct 1' => 0xff3a,
+ 'Half non-punct 2' => 0xff5a,
+ 'Half non-punct 3' => 0xffdc,
+ 'Hangul Syllables' => 0xd7af,
+ 'Hiragana' => 0x309f,
+ 'Katakana' => 0x30ff,
+ 'Katakana Ext' => 0x31ff,
+ 'CJK Reserve 1' => 0x2fffd,
+ 'CJK Reserve 2' => 0x3fffd,
+ 'Bomofo' => 0x312f,
+ 'Bomofo Ext' => 0x31b7,
+ 'Lisu' => 0xa4fd,
+ 'Yi' => 0xa48f,
+ );
+
+ // Generate characters consisting of starts, midpoints, and ends.
+ $chars = array();
+ $charcodes = array();
+ foreach ($starts as $key => $value) {
+ $charcodes[] = $starts[$key];
+ $chars[] = $this->code2utf($starts[$key]);
+ $mid = round(0.5 * ($starts[$key] + $ends[$key]));
+ $charcodes[] = $mid;
+ $chars[] = $this->code2utf($mid);
+ $charcodes[] = $ends[$key];
+ $chars[] = $this->code2utf($ends[$key]);
+ }
+
+ // Merge into a string and tokenize.
+ $string = implode('', $chars);
+ $out = trim(search_simplify($string));
+ $expected = drupal_strtolower(implode(' ', $chars));
+
+ // Verify that the output matches what we expect.
+ $this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
+ }
+
+ /**
+ * Verifies that strings of non-CJK characters are not tokenized.
+ *
+ * This is just a sanity check - it verifies that strings of letters are
+ * not tokenized.
+ */
+ function testNoTokenizer() {
+ // Set the minimum word size to 1 (to split all CJK characters) and make
+ // sure CJK tokenizing is turned on.
+ variable_set('minimum_word_size', 1);
+ variable_set('overlap_cjk', TRUE);
+ $this->refreshVariables();
+
+ $letters = 'abcdefghijklmnopqrstuvwxyz';
+ $out = trim(search_simplify($letters));
+
+ $this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
+ }
+
+ /**
+ * Like PHP chr() function, but for unicode characters.
+ *
+ * chr() only works for ASCII characters up to character 255. This function
+ * converts a number to the corresponding unicode character. Adapted from
+ * functions supplied in comments on several functions on php.net.
+ */
+ function code2utf($num) {
+ if ($num < 128) {
+ return chr($num);
+ }
+
+ if ($num < 2048) {
+ return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
+ }
+
+ if ($num < 65536) {
+ return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
+ }
+
+ if ($num < 2097152) {
+ return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
+ }
+
+ return '';
+ }
+}
+
+/**
+ * Tests that we can embed a form in search results and submit it.
+ */
+class SearchEmbedForm extends DrupalWebTestCase {
+ /**
+ * Node used for testing.
+ */
+ public $node;
+
+ /**
+ * Count of how many times the form has been submitted.
+ */
+ public $submit_count = 0;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Embedded forms',
+ 'description' => 'Verifies that a form embedded in search results works',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search', 'search_embedded_form');
+
+ // Create a user and a node, and update the search index.
+ $test_user = $this->drupalCreateUser(array('access content', 'search content', 'administer nodes'));
+ $this->drupalLogin($test_user);
+
+ $this->node = $this->drupalCreateNode();
+
+ node_update_index();
+ search_update_totals();
+
+ // Set up a dummy initial count of times the form has been submitted.
+ $this->submit_count = 12;
+ variable_set('search_embedded_form_submitted', $this->submit_count);
+ $this->refreshVariables();
+ }
+
+ /**
+ * Tests that the embedded form appears and can be submitted.
+ */
+ function testEmbeddedForm() {
+ // First verify we can submit the form from the module's page.
+ $this->drupalPost('search_embedded_form',
+ array('name' => 'John'),
+ t('Send away'));
+ $this->assertText(t('Test form was submitted'), 'Form message appears');
+ $count = variable_get('search_embedded_form_submitted', 0);
+ $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
+ $this->submit_count = $count;
+
+ // Now verify that we can see and submit the form from the search results.
+ $this->drupalGet('search/node/' . $this->node->title);
+ $this->assertText(t('Your name'), 'Form is visible');
+ $this->drupalPost('search/node/' . $this->node->title,
+ array('name' => 'John'),
+ t('Send away'));
+ $this->assertText(t('Test form was submitted'), 'Form message appears');
+ $count = variable_get('search_embedded_form_submitted', 0);
+ $this->assertEqual($this->submit_count + 1, $count, 'Form submission count is correct');
+ $this->submit_count = $count;
+
+ // Now verify that if we submit the search form, it doesn't count as
+ // our form being submitted.
+ $this->drupalPost('search',
+ array('keys' => 'foo'),
+ t('Search'));
+ $this->assertNoText(t('Test form was submitted'), 'Form message does not appear');
+ $count = variable_get('search_embedded_form_submitted', 0);
+ $this->assertEqual($this->submit_count, $count, 'Form submission count is correct');
+ $this->submit_count = $count;
+ }
+}
+
+/**
+ * Tests that hook_search_page runs.
+ */
+class SearchPageOverride extends DrupalWebTestCase {
+ public $search_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search page override',
+ 'description' => 'Verify that hook_search_page can override search page display.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search', 'search_extra_type');
+
+ // Login as a user that can create and search content.
+ $this->search_user = $this->drupalCreateUser(array('search content', 'administer search'));
+ $this->drupalLogin($this->search_user);
+
+ // Enable the extra type module for searching.
+ variable_set('search_active_modules', array('node' => 'node', 'user' => 'user', 'search_extra_type' => 'search_extra_type'));
+ menu_rebuild();
+ }
+
+ function testSearchPageHook() {
+ $keys = 'bike shed ' . $this->randomName();
+ $this->drupalGet("search/dummy_path/{$keys}");
+ $this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
+ $this->assertText('Test page text is here', 'Page override is working');
+ }
+}
+
+/**
+ * Test node search with multiple languages.
+ */
+class SearchLanguageTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search language selection',
+ 'description' => 'Tests advanced search with different languages enabled.',
+ 'group' => 'Search',
+ );
+ }
+
+ /**
+ * Implementation setUp().
+ */
+ function setUp() {
+ parent::setUp('search', 'locale');
+
+ // Create and login user.
+ $test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages'));
+ $this->drupalLogin($test_user);
+ }
+
+ function testLanguages() {
+ // Check that there are initially no languages displayed.
+ $this->drupalGet('search/node');
+ $this->assertNoText(t('Languages'), 'No languages to choose from.');
+
+ // Add predefined language.
+ $edit = array('langcode' => 'fr');
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+ $this->assertText('fr', 'Language added successfully.');
+
+ // Now we should have languages displayed.
+ $this->drupalGet('search/node');
+ $this->assertText(t('Languages'), 'Languages displayed to choose from.');
+ $this->assertText(t('English'), 'English is a possible choice.');
+ $this->assertText(t('French'), 'French is a possible choice.');
+
+ // Ensure selecting no language does not make the query different.
+ $this->drupalPost('search/node', array(), t('Advanced search'));
+ $this->assertEqual($this->getUrl(), url('search/node/', array('absolute' => TRUE)), 'Correct page redirection, no language filtering.');
+
+ // Pick French and ensure it is selected.
+ $edit = array('language[fr]' => TRUE);
+ $this->drupalPost('search/node', $edit, t('Advanced search'));
+ $this->assertFieldByXPath('//input[@name="keys"]', 'language:fr', 'Language filter added to query.');
+
+ // Change the default language and disable English.
+ $path = 'admin/config/regional/language';
+ $this->drupalGet($path);
+ $this->assertFieldChecked('edit-site-default-en', 'English is the default language.');
+ $edit = array('site_default' => 'fr');
+ $this->drupalPost(NULL, $edit, t('Save configuration'));
+ $this->assertNoFieldChecked('edit-site-default-en', 'Default language updated.');
+ $edit = array('enabled[en]' => FALSE);
+ $this->drupalPost('admin/config/regional/language', $edit, t('Save configuration'));
+ $this->assertNoFieldChecked('edit-enabled-en', 'Language disabled.');
+
+ // Check that there are again no languages displayed.
+ $this->drupalGet('search/node');
+ $this->assertNoText(t('Languages'), 'No languages to choose from.');
+ }
+}
+
+/**
+ * Tests node search with node access control.
+ */
+class SearchNodeAccessTest extends DrupalWebTestCase {
+ public $test_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search and node access',
+ 'description' => 'Tests search functionality with node access control.',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search', 'node_access_test');
+ node_access_rebuild();
+
+ // Create a test user and log in.
+ $this->test_user = $this->drupalCreateUser(array('access content', 'search content', 'use advanced search'));
+ $this->drupalLogin($this->test_user);
+ }
+
+ /**
+ * Tests that search returns results with punctuation in the search phrase.
+ */
+ function testPhraseSearchPunctuation() {
+ $node = $this->drupalCreateNode(array('body' => array(LANGUAGE_NONE => array(array('value' => "The bunny's ears were fuzzy.")))));
+
+ // Update the search index.
+ module_invoke_all('update_index');
+ search_update_totals();
+
+ // Refresh variables after the treatment.
+ $this->refreshVariables();
+
+ // Submit a phrase wrapped in double quotes to include the punctuation.
+ $edit = array('keys' => '"bunny\'s"');
+ $this->drupalPost('search/node', $edit, t('Search'));
+ $this->assertText($node->title);
+ }
+}
+
+/**
+ * Tests searching with locale values set.
+ */
+class SearchSetLocaleTest extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Search with numeric locale set',
+ 'description' => 'Check that search works with numeric locale settings',
+ 'group' => 'Search',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('search');
+
+ // Create a simple node so something will be put in the index.
+ $info = array(
+ 'body' => array(LANGUAGE_NONE => array(array('value' => 'Tapir'))),
+ );
+ $this->drupalCreateNode($info);
+
+ // Run cron to index.
+ $this->cronRun();
+ }
+
+ /**
+ * Verify that search works with a numeric locale set.
+ */
+ public function testSearchWithNumericLocale() {
+ // French decimal point is comma.
+ setlocale(LC_NUMERIC, 'fr_FR');
+
+ // An exception will be thrown if a float in the wrong format occurs in the
+ // query to the database, so an assertion is not necessary here.
+ db_select('search_index', 'i')
+ ->extend('searchquery')
+ ->searchexpression('tapir', 'node')
+ ->execute();
+ }
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/tests/UnicodeTest.txt b/kolab.org/www/drupal-7.26/modules/search/tests/UnicodeTest.txt
new file mode 100644
index 0000000..af8a65c
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/tests/UnicodeTest.txt
@@ -0,0 +1,333 @@
+
+ !"#$%&'()*+,-./
+0123456789
+:;<=>?@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
+[\]^_`
+abcdefghijklmnopqrstuvwxyz
+{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©
+«¬­®¯°±
+²³
+¶·¸
+¹º
+¼½¾
+¿
+ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ
+ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö
+øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ
+˂˃˄˅
+ˆˇˈˉˊˋˌˍˎˏːˑ
+˒˓˔˕˖˗˘˙˚˛˜˝˞˟
+ˠˡˢˣˤ
+˥˦˧˨˩˪˫
+˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿
+̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ
+Ͷͷͺͻͼͽ
+;΄΅
+ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ
+ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ
+҃҄҅҆҇҈҉ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖՙ
+՚՛՜՝՞՟
+աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆև
+։֊
+ְֱֲֳִֵֶַָֹֺֻּֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯
+ֿ
+ׁׂ
+ׅׄ
+ׇאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ
+׳״؀؁؂؃؆؇؈؉؊؋،؍؎؏
+ؘؙؚؐؑؒؓؔؕؖؗ
+؛؞؟
+ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّْٕٖٜٓٔٗ٘ٙٚٛٝٞ٠١٢٣٤٥٦٧٨٩
+٪٫٬٭
+ٮٯٰٱٲٳٴٵٶٷٸٹٺٻټٽپٿڀځڂڃڄڅچڇڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙښڛڜڝڞڟڠڡڢڣڤڥڦڧڨکڪګڬڭڮگڰڱڲڳڴڵڶڷڸڹںڻڼڽھڿۀہۂۃۄۅۆۇۈۉۊۋیۍێۏېۑےۓ
+ەۖۗۘۙۚۛۜ
+۞ۣ۟۠ۡۢۤۥۦۧۨ
+۪ۭ۫۬ۮۯ۰۱۲۳۴۵۶۷۸۹ۺۻۼ
+۽۾
+ۿ
+܀܁܂܃܄܅܆܇܈܉܊܋܌܍܏
+ܐܑܒܓܔܕܖܗܘܙܚܛܜܝܞܟܠܡܢܣܤܥܦܧܨܩܪܫܬܭܮܯܱܴܷܸܹܻܼܾ݂݄݆݈ܰܲܳܵܶܺܽܿ݀݁݃݅݇݉݊ݍݎݏݐݑݒݓݔݕݖݗݘݙݚݛݜݝݞݟݠݡݢݣݤݥݦݧݨݩݪݫݬݭݮݯݰݱݲݳݴݵݶݷݸݹݺݻݼݽݾݿހށނރބޅކއވމފދތލގޏސޑޒޓޔޕޖޗޘޙޚޛޜޝޞޟޠޡޢޣޤޥަާިީުޫެޭޮޯްޱ߀߁߂߃߄߅߆߇߈߉ߊߋߌߍߎߏߐߑߒߓߔߕߖߗߘߙߚߛߜߝߞߟߠߡߢߣߤߥߦߧߨߩߪ߲߫߬߭߮߯߰߱߳ߴߵ
+߶߷߸߹
+ߺࠀࠁࠂࠃࠄࠅࠆࠇࠈࠉࠊࠋࠌࠍࠎࠏࠐࠑࠒࠓࠔࠕࠖࠗ࠘࠙ࠚࠛࠜࠝࠞࠟࠠࠡࠢࠣࠤࠥࠦࠧࠨࠩࠪࠫࠬ࠭
+࠰࠱࠲࠳࠴࠵࠶࠷࠸࠹࠺࠻࠼࠽࠾
+ऀँंःऄअआइईउऊऋऌऍऎएऐऑऒओऔकखगघङचछजझञटठडढणतथदधनऩपफबभमयरऱलळऴवशषसह़ऽािीुूृॄॅॆेैॉॊोौ्ॎॐ॒॑॓॔ॕक़ख़ग़ज़ड़ढ़फ़य़ॠॡॢॣ
+।॥
+०१२३४५६७८९
+॰
+ॱॲॹॺॻॼॽॾॿঁংঃঅআইঈউঊঋঌএঐওঔকখগঘঙচছজঝঞটঠডঢণতথদধনপফবভমযরলশষসহ়ঽািীুূৃৄেৈোৌ্ৎৗড়ঢ়য়ৠৡৢৣ০১২৩৪৫৬৭৮৯ৰৱ
+৲৳
+৴৵৶৷৸৹
+৺৻
+ਁਂਃਅਆਇਈਉਊਏਐਓਔਕਖਗਘਙਚਛਜਝਞਟਠਡਢਣਤਥਦਧਨਪਫਬਭਮਯਰਲਲ਼ਵਸ਼ਸਹ਼ਾਿੀੁੂੇੈੋੌ੍ੑਖ਼ਗ਼ਜ਼ੜਫ਼੦੧੨੩੪੫੬੭੮੯ੰੱੲੳੴੵઁંઃઅઆઇઈઉઊઋઌઍએઐઑઓઔકખગઘઙચછજઝઞટઠડઢણતથદધનપફબભમયરલળવશષસહ઼ઽાિીુૂૃૄૅેૈૉોૌ્ૐૠૡૢૣ૦૧૨૩૪૫૬૭૮૯
+૱
+ଁଂଃଅଆଇଈଉଊଋଌଏଐଓଔକଖଗଘଙଚଛଜଝଞଟଠଡଢଣତଥଦଧନପଫବଭମଯରଲଳଵଶଷସହ଼ଽାିୀୁୂୃୄେୈୋୌ୍ୖୗଡ଼ଢ଼ୟୠୡୢୣ୦୧୨୩୪୫୬୭୮୯
+୰
+ୱஂஃஅஆஇஈஉஊஎஏஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஶஷஸஹாிீுூெேைொோௌ்ௐௗ௦௧௨௩௪௫௬௭௮௯௰௱௲
+௳௴௵௶௷௸௹௺
+ఁంఃఅఆఇఈఉఊఋఌఎఏఐఒఓఔకఖగఘఙచఛజఝఞటఠడఢణతథదధనపఫబభమయరఱలళవశషసహఽాిీుూృౄెేైొోౌ్ౕౖౘౙౠౡౢౣ౦౧౨౩౪౫౬౭౮౯౸౹౺౻౼౽౾
+౿
+ಂಃಅಆಇಈಉಊಋಌಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಱಲಳವಶಷಸಹ಼ಽಾಿೀುೂೃೄೆೇೈೊೋೌ್ೕೖೞೠೡೢೣ೦೧೨೩೪೫೬೭೮೯
+ೱೲ
+ംഃഅആഇഈഉഊഋഌഎഏഐഒഓഔകഖഗഘങചഛജഝഞടഠഡഢണതഥദധനപഫബഭമയരറലളഴവശഷസഹഽാിീുൂൃൄെേൈൊോൌ്ൗൠൡൢൣ൦൧൨൩൪൫൬൭൮൯൰൱൲൳൴൵
+൹
+ൺൻർൽൾൿංඃඅආඇඈඉඊඋඌඍඎඏඐඑඒඓඔඕඖකඛගඝඞඟචඡජඣඤඥඦටඨඩඪණඬතථදධනඳපඵබභමඹයරලවශෂසහළෆ්ාැෑිීුූෘෙේෛොෝෞෟෲෳ
+෴
+กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู
+฿
+เแโใไๅๆ็่้๊๋์ํ๎
+๏
+๐๑๒๓๔๕๖๗๘๙
+๚๛
+ກຂຄງຈຊຍດຕຖທນບປຜຝພຟມຢຣລວສຫອຮຯະັາຳິີຶືຸູົຼຽເແໂໃໄໆ່້໊໋໌ໍ໐໑໒໓໔໕໖໗໘໙ໜໝༀ
+༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗
+༘༙
+༚༛༜༝༞༟
+༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳
+༴
+༵
+༶
+༷
+༸
+༹
+༺༻༼༽
+༾༿ཀཁགགྷངཅཆཇཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ
+྅
+྆྇ྈྉྊྋྐྑྒྒྷྔྕྖྗྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ
+྾྿࿀࿁࿂࿃࿄࿅
+࿆
+࿇࿈࿉࿊࿋࿌࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘
+ကခဂဃငစဆဇဈဉညဋဌဍဎဏတထဒဓနပဖဗဘမယရလဝသဟဠအဢဣဤဥဦဧဨဩဪါာိီုူေဲဳဴဵံ့း္်ျြွှဿ၀၁၂၃၄၅၆၇၈၉
+၊။၌၍၎၏
+ၐၑၒၓၔၕၖၗၘၙၚၛၜၝၞၟၠၡၢၣၤၥၦၧၨၩၪၫၬၭၮၯၰၱၲၳၴၵၶၷၸၹၺၻၼၽၾၿႀႁႂႃႄႅႆႇႈႉႊႋႌႍႎႏ႐႑႒႓႔႕႖႗႘႙ႚႛႜႝ
+႞႟
+ႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅაბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰჱჲჳჴჵჶჷჸჹჺ
+჻
+ჼᄀᄁᄂᄃᄄᄅᄆᄇᄈᄉᄊᄋᄌᄍᄎᄏᄐᄑᄒᄓᄔᄕᄖᄗᄘᄙᄚᄛᄜᄝᄞᄟᄠᄡᄢᄣᄤᄥᄦᄧᄨᄩᄪᄫᄬᄭᄮᄯᄰᄱᄲᄳᄴᄵᄶᄷᄸᄹᄺᄻᄼᄽᄾᄿᅀᅁᅂᅃᅄᅅᅆᅇᅈᅉᅊᅋᅌᅍᅎᅏᅐᅑᅒᅓᅔᅕᅖᅗᅘᅙᅚᅛᅜᅝᅞᅟᅠᅡᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵᅶᅷᅸᅹᅺᅻᅼᅽᅾᅿᆀᆁᆂᆃᆄᆅᆆᆇᆈᆉᆊᆋᆌᆍᆎᆏᆐᆑᆒᆓᆔᆕᆖᆗᆘᆙᆚᆛᆜᆝᆞᆟᆠᆡᆢᆣᆤᆥᆦᆧᆨᆩᆪᆫᆬᆭᆮᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸᆹᆺᆻᆼᆽᆾᆿᇀᇁᇂᇃᇄᇅᇆᇇᇈᇉᇊᇋᇌᇍᇎᇏᇐᇑᇒᇓᇔᇕᇖᇗᇘᇙᇚᇛᇜᇝᇞᇟᇠᇡᇢᇣᇤᇥᇦᇧᇨᇩᇪᇫᇬᇭᇮᇯᇰᇱᇲᇳᇴᇵᇶᇷᇸᇹᇺᇻᇼᇽᇾᇿሀሁሂሃሄህሆሇለሉሊላሌልሎሏሐሑሒሓሔሕሖሗመሙሚማሜምሞሟሠሡሢሣሤሥሦሧረሩሪራሬርሮሯሰሱሲሳሴስሶሷሸሹሺሻሼሽሾሿቀቁቂቃቄቅቆቇቈቊቋቌቍቐቑቒቓቔቕቖቘቚቛቜቝበቡቢባቤብቦቧቨቩቪቫቬቭቮቯተቱቲታቴትቶቷቸቹቺቻቼችቾቿኀኁኂኃኄኅኆኇኈኊኋኌኍነኑኒናኔንኖኗኘኙኚኛኜኝኞኟአኡኢኣኤእኦኧከኩኪካኬክኮኯኰኲኳኴኵኸኹኺኻኼኽኾዀዂዃዄዅወዉዊዋዌውዎዏዐዑዒዓዔዕዖዘዙዚዛዜዝዞዟዠዡዢዣዤዥዦዧየዩዪያዬይዮዯደዱዲዳዴድዶዷዸዹዺዻዼዽዾዿጀጁጂጃጄጅጆጇገጉጊጋጌግጎጏጐጒጓጔጕጘጙጚጛጜጝጞጟጠጡጢጣጤጥጦጧጨጩጪጫጬጭጮጯጰጱጲጳጴጵጶጷጸጹጺጻጼጽጾጿፀፁፂፃፄፅፆፇፈፉፊፋፌፍፎፏፐፑፒፓፔፕፖፗፘፙፚ፟
+፠፡።፣፤፥፦፧፨
+፩፪፫፬፭፮፯፰፱፲፳፴፵፶፷፸፹፺፻፼ᎀᎁᎂᎃᎄᎅᎆᎇᎈᎉᎊᎋᎌᎍᎎᎏ
+᎐᎑᎒᎓᎔᎕᎖᎗᎘᎙
+ᎠᎡᎢᎣᎤᎥᎦᎧᎨᎩᎪᎫᎬᎭᎮᎯᎰᎱᎲᎳᎴᎵᎶᎷᎸᎹᎺᎻᎼᎽᎾᎿᏀᏁᏂᏃᏄᏅᏆᏇᏈᏉᏊᏋᏌᏍᏎᏏᏐᏑᏒᏓᏔᏕᏖᏗᏘᏙᏚᏛᏜᏝᏞᏟᏠᏡᏢᏣᏤᏥᏦᏧᏨᏩᏪᏫᏬᏭᏮᏯᏰᏱᏲᏳᏴ
+᐀
+ᐁᐂᐃᐄᐅᐆᐇᐈᐉᐊᐋᐌᐍᐎᐏᐐᐑᐒᐓᐔᐕᐖᐗᐘᐙᐚᐛᐜᐝᐞᐟᐠᐡᐢᐣᐤᐥᐦᐧᐨᐩᐪᐫᐬᐭᐮᐯᐰᐱᐲᐳᐴᐵᐶᐷᐸᐹᐺᐻᐼᐽᐾᐿᑀᑁᑂᑃᑄᑅᑆᑇᑈᑉᑊᑋᑌᑍᑎᑏᑐᑑᑒᑓᑔᑕᑖᑗᑘᑙᑚᑛᑜᑝᑞᑟᑠᑡᑢᑣᑤᑥᑦᑧᑨᑩᑪᑫᑬᑭᑮᑯᑰᑱᑲᑳᑴᑵᑶᑷᑸᑹᑺᑻᑼᑽᑾᑿᒀᒁᒂᒃᒄᒅᒆᒇᒈᒉᒊᒋᒌᒍᒎᒏᒐᒑᒒᒓᒔᒕᒖᒗᒘᒙᒚᒛᒜᒝᒞᒟᒠᒡᒢᒣᒤᒥᒦᒧᒨᒩᒪᒫᒬᒭᒮᒯᒰᒱᒲᒳᒴᒵᒶᒷᒸᒹᒺᒻᒼᒽᒾᒿᓀᓁᓂᓃᓄᓅᓆᓇᓈᓉᓊᓋᓌᓍᓎᓏᓐᓑᓒᓓᓔᓕᓖᓗᓘᓙᓚᓛᓜᓝᓞᓟᓠᓡᓢᓣᓤᓥᓦᓧᓨᓩᓪᓫᓬᓭᓮᓯᓰᓱᓲᓳᓴᓵᓶᓷᓸᓹᓺᓻᓼᓽᓾᓿᔀᔁᔂᔃᔄᔅᔆᔇᔈᔉᔊᔋᔌᔍᔎᔏᔐᔑᔒᔓᔔᔕᔖᔗᔘᔙᔚᔛᔜᔝᔞᔟᔠᔡᔢᔣᔤᔥᔦᔧᔨᔩᔪᔫᔬᔭᔮᔯᔰᔱᔲᔳᔴᔵᔶᔷᔸᔹᔺᔻᔼᔽᔾᔿᕀᕁᕂᕃᕄᕅᕆᕇᕈᕉᕊᕋᕌᕍᕎᕏᕐᕑᕒᕓᕔᕕᕖᕗᕘᕙᕚᕛᕜᕝᕞᕟᕠᕡᕢᕣᕤᕥᕦᕧᕨᕩᕪᕫᕬᕭᕮᕯᕰᕱᕲᕳᕴᕵᕶᕷᕸᕹᕺᕻᕼᕽᕾᕿᖀᖁᖂᖃᖄᖅᖆᖇᖈᖉᖊᖋᖌᖍᖎᖏᖐᖑᖒᖓᖔᖕᖖᖗᖘᖙᖚᖛᖜᖝᖞᖟᖠᖡᖢᖣᖤᖥᖦᖧᖨᖩᖪᖫᖬᖭᖮᖯᖰᖱᖲᖳᖴᖵᖶᖷᖸᖹᖺᖻᖼᖽᖾᖿᗀᗁᗂᗃᗄᗅᗆᗇᗈᗉᗊᗋᗌᗍᗎᗏᗐᗑᗒᗓᗔᗕᗖᗗᗘᗙᗚᗛᗜᗝᗞᗟᗠᗡᗢᗣᗤᗥᗦᗧᗨᗩᗪᗫᗬᗭᗮᗯᗰᗱᗲᗳᗴᗵᗶᗷᗸᗹᗺᗻᗼᗽᗾᗿᘀᘁᘂᘃᘄᘅᘆᘇᘈᘉᘊᘋᘌᘍᘎᘏᘐᘑᘒᘓᘔᘕᘖᘗᘘᘙᘚᘛᘜᘝᘞᘟᘠᘡᘢᘣᘤᘥᘦᘧᘨᘩᘪᘫᘬᘭᘮᘯᘰᘱᘲᘳᘴᘵᘶᘷᘸᘹᘺᘻᘼᘽᘾᘿᙀᙁᙂᙃᙄᙅᙆᙇᙈᙉᙊᙋᙌᙍᙎᙏᙐᙑᙒᙓᙔᙕᙖᙗᙘᙙᙚᙛᙜᙝᙞᙟᙠᙡᙢᙣᙤᙥᙦᙧᙨᙩᙪᙫᙬ
+᙭᙮
+ᙯᙰᙱᙲᙳᙴᙵᙶᙷᙸᙹᙺᙻᙼᙽᙾᙿ
+ 
+ᚁᚂᚃᚄᚅᚆᚇᚈᚉᚊᚋᚌᚍᚎᚏᚐᚑᚒᚓᚔᚕᚖᚗᚘᚙᚚ
+᚛᚜
+ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩᚪᚫᚬᚭᚮᚯᚰᚱᚲᚳᚴᚵᚶᚷᚸᚹᚺᚻᚼᚽᚾᚿᛀᛁᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋᛌᛍᛎᛏᛐᛑᛒᛓᛔᛕᛖᛗᛘᛙᛚᛛᛜᛝᛞᛟᛠᛡᛢᛣᛤᛥᛦᛧᛨᛩᛪ
+᛫᛬᛭
+ᛮᛯᛰᜀᜁᜂᜃᜄᜅᜆᜇᜈᜉᜊᜋᜌᜎᜏᜐᜑᜒᜓ᜔ᜠᜡᜢᜣᜤᜥᜦᜧᜨᜩᜪᜫᜬᜭᜮᜯᜰᜱᜲᜳ᜴
+᜵᜶
+ᝀᝁᝂᝃᝄᝅᝆᝇᝈᝉᝊᝋᝌᝍᝎᝏᝐᝑᝒᝓᝠᝡᝢᝣᝤᝥᝦᝧᝨᝩᝪᝫᝬᝮᝯᝰᝲᝳកខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអឣឤឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ
+឴឵
+ាិីឹឺុូួើឿៀេែៃោៅំះៈ៉៊់៌៍៎៏័៑្៓
+។៕៖
+ៗ
+៘៙៚៛
+ៜ៝០១២៣៤៥៦៧៨៩៰៱៲៳៴៵៶៷៸៹
+᠀᠁᠂᠃᠄᠅᠆᠇᠈᠉᠊
+᠋᠌᠍
+᠎
+᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙ᠠᠡᠢᠣᠤᠥᠦᠧᠨᠩᠪᠫᠬᠭᠮᠯᠰᠱᠲᠳᠴᠵᠶᠷᠸᠹᠺᠻᠼᠽᠾᠿᡀᡁᡂᡃᡄᡅᡆᡇᡈᡉᡊᡋᡌᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡘᡙᡚᡛᡜᡝᡞᡟᡠᡡᡢᡣᡤᡥᡦᡧᡨᡩᡪᡫᡬᡭᡮᡯᡰᡱᡲᡳᡴᡵᡶᡷᢀᢁᢂᢃᢄᢅᢆᢇᢈᢉᢊᢋᢌᢍᢎᢏᢐᢑᢒᢓᢔᢕᢖᢗᢘᢙᢚᢛᢜᢝᢞᢟᢠᢡᢢᢣᢤᢥᢦᢧᢨᢩᢪᢰᢱᢲᢳᢴᢵᢶᢷᢸᢹᢺᢻᢼᢽᢾᢿᣀᣁᣂᣃᣄᣅᣆᣇᣈᣉᣊᣋᣌᣍᣎᣏᣐᣑᣒᣓᣔᣕᣖᣗᣘᣙᣚᣛᣜᣝᣞᣟᣠᣡᣢᣣᣤᣥᣦᣧᣨᣩᣪᣫᣬᣭᣮᣯᣰᣱᣲᣳᣴᣵᤀᤁᤂᤃᤄᤅᤆᤇᤈᤉᤊᤋᤌᤍᤎᤏᤐᤑᤒᤓᤔᤕᤖᤗᤘᤙᤚᤛᤜᤠᤡᤢᤣᤤᤥᤦᤧᤨᤩᤪᤫᤰᤱᤲᤳᤴᤵᤶᤷᤸ᤻᤹᤺
+᥀᥄᥅
+᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏ᥐᥑᥒᥓᥔᥕᥖᥗᥘᥙᥚᥛᥜᥝᥞᥟᥠᥡᥢᥣᥤᥥᥦᥧᥨᥩᥪᥫᥬᥭᥰᥱᥲᥳᥴᦀᦁᦂᦃᦄᦅᦆᦇᦈᦉᦊᦋᦌᦍᦎᦏᦐᦑᦒᦓᦔᦕᦖᦗᦘᦙᦚᦛᦜᦝᦞᦟᦠᦡᦢᦣᦤᦥᦦᦧᦨᦩᦪᦫᦰᦱᦲᦳᦴᦵᦶᦷᦸᦹᦺᦻᦼᦽᦾᦿᧀᧁᧂᧃᧄᧅᧆᧇᧈᧉ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᧚
+᧞᧟᧠᧡᧢᧣᧤᧥᧦᧧᧨᧩᧪᧫᧬᧭᧮᧯᧰᧱᧲᧳᧴᧵᧶᧷᧸᧹᧺᧻᧼᧽᧾᧿
+ᨀᨁᨂᨃᨄᨅᨆᨇᨈᨉᨊᨋᨌᨍᨎᨏᨐᨑᨒᨓᨔᨕᨖᨘᨗᨙᨚᨛ
+᨞᨟
+ᨠᨡᨢᨣᨤᨥᨦᨧᨨᨩᨪᨫᨬᨭᨮᨯᨰᨱᨲᨳᨴᨵᨶᨷᨸᨹᨺᨻᨼᨽᨾᨿᩀᩁᩂᩃᩄᩅᩆᩇᩈᩉᩊᩋᩌᩍᩎᩏᩐᩑᩒᩓᩔᩕᩖᩗᩘᩙᩚᩛᩜᩝᩞ᩠ᩡᩢᩣᩤᩥᩦᩧᩨᩩᩪᩫᩬᩭᩮᩯᩰᩱᩲᩳᩴ᩿᩵᩶᩷᩸᩹᩺᩻᩼᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
+᪠᪡᪢᪣᪤᪥᪦
+ᪧ
+᪨᪩᪪᪫᪬᪭
+ᬀᬁᬂᬃᬄᬅᬆᬇᬈᬉᬊᬋᬌᬍᬎᬏᬐᬑᬒᬓᬔᬕᬖᬗᬘᬙᬚᬛᬜᬝᬞᬟᬠᬡᬢᬣᬤᬥᬦᬧᬨᬩᬪᬫᬬᬭᬮᬯᬰᬱᬲᬳ᬴ᬵᬶᬷᬸᬹᬺᬻᬼᬽᬾᬿᭀᭁᭂᭃ᭄ᭅᭆᭇᭈᭉᭊᭋ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
+᭚᭛᭜᭝᭞᭟᭠᭡᭢᭣᭤᭥᭦᭧᭨᭩᭪
+᭬᭫᭭᭮᭯᭰᭱᭲᭳
+᭴᭵᭶᭷᭸᭹᭺᭻᭼
+ᮀᮁᮂᮃᮄᮅᮆᮇᮈᮉᮊᮋᮌᮍᮎᮏᮐᮑᮒᮓᮔᮕᮖᮗᮘᮙᮚᮛᮜᮝᮞᮟᮠᮡᮢᮣᮤᮥᮦᮧᮨᮩ᮪ᮮᮯ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹ᰀᰁᰂᰃᰄᰅᰆᰇᰈᰉᰊᰋᰌᰍᰎᰏᰐᰑᰒᰓᰔᰕᰖᰗᰘᰙᰚᰛᰜᰝᰞᰟᰠᰡᰢᰣᰤᰥᰦᰧᰨᰩᰪᰫᰬᰭᰮᰯᰰᰱᰲᰳᰴᰵᰶ᰷
+᰻᰼᰽᰾᰿
+᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉ᱍᱎᱏ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙ᱚᱛᱜᱝᱞᱟᱠᱡᱢᱣᱤᱥᱦᱧᱨᱩᱪᱫᱬᱭᱮᱯᱰᱱᱲᱳᱴᱵᱶᱷᱸᱹᱺᱻᱼᱽ
+᱾᱿
+᳐᳑᳒
+᳓
+᳔᳕᳖᳗᳘᳙᳜᳝᳞᳟᳚᳛᳠᳡᳢᳣᳤᳥᳦᳧᳨ᳩᳪᳫᳬ᳭ᳮᳯᳰᳱᳲᴀᴁᴂᴃᴄᴅᴆᴇᴈᴉᴊᴋᴌᴍᴎᴏᴐᴑᴒᴓᴔᴕᴖᴗᴘᴙᴚᴛᴜᴝᴞᴟᴠᴡᴢᴣᴤᴥᴦᴧᴨᴩᴪᴫᴬᴭᴮᴯᴰᴱᴲᴳᴴᴵᴶᴷᴸᴹᴺᴻᴼᴽᴾᴿᵀᵁᵂᵃᵄᵅᵆᵇᵈᵉᵊᵋᵌᵍᵎᵏᵐᵑᵒᵓᵔᵕᵖᵗᵘᵙᵚᵛᵜᵝᵞᵟᵠᵡᵢᵣᵤᵥᵦᵧᵨᵩᵪᵫᵬᵭᵮᵯᵰᵱᵲᵳᵴᵵᵶᵷᵸᵹᵺᵻᵼᵽᵾᵿᶀᶁᶂᶃᶄᶅᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶛᶜᶝᶞᶟᶠᶡᶢᶣᶤᶥᶦᶧᶨᶩᶪᶫᶬᶭᶮᶯᶰᶱᶲᶳᶴᶵᶶᶷᶸᶹᶺᶻᶼᶽᶾᶿ᷐᷎᷂᷊᷏᷽᷿᷀᷁᷃᷄᷅᷆᷇᷈᷉᷋᷌᷑᷒ᷓᷔᷕᷖᷗᷘᷙᷚᷛᷜᷝᷞᷟᷠᷡᷢᷣᷤᷥᷦ᷾᷍ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẚẛẜẝẞẟẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹỺỻỼỽỾỿἀἁἂἃἄἅἆἇἈἉἊἋἌἍἎἏἐἑἒἓἔἕἘἙἚἛἜἝἠἡἢἣἤἥἦἧἨἩἪἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿὀὁὂὃὄὅὈὉὊὋὌὍὐὑὒὓὔὕὖὗὙὛὝὟὠὡὢὣὤὥὦὧὨὩὪὫὬὭὮὯὰάὲέὴήὶίὸόὺύὼώᾀᾁᾂᾃᾄᾅᾆᾇᾈᾉᾊᾋᾌᾍᾎᾏᾐᾑᾒᾓᾔᾕᾖᾗᾘᾙᾚᾛᾜᾝᾞᾟᾠᾡᾢᾣᾤᾥᾦᾧᾨᾩᾪᾫᾬᾭᾮᾯᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺΆᾼ
+᾽
+ι
+᾿῀῁
+ῂῃῄῆῇῈΈῊΉῌ
+῍῎῏
+ῐῑῒΐῖῗῘῙῚΊ
+῝῞῟
+ῠῡῢΰῤῥῦῧῨῩῪΎῬ
+῭΅`
+ῲῳῴῶῷῸΌῺΏῼ
+´῾           ​‌‍‎‏‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧

‪‫‬‭‮ ‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞ ⁠⁡⁢⁣⁤
+⁰ⁱ⁴⁵⁶⁷⁸⁹
+⁺⁻⁼⁽⁾
+ⁿ₀₁₂₃₄₅₆₇₈₉
+₊₋₌₍₎
+ₐₑₒₓₔ
+₠₡₢₣₤₥₦₧₨₩₪₫€₭₮₯₰₱₲₳₴₵₶₷₸
+⃒⃓⃘⃙⃚⃐⃑⃔⃕⃖⃗⃛⃜⃝⃞⃟⃠⃡⃢⃣⃤⃥⃦⃪⃫⃨⃬⃭⃮⃯⃧⃩⃰
+℀℁
+ℂ
+℃℄℅℆
+ℇ
+℈℉
+ℊℋℌℍℎℏℐℑℒℓ
+℔
+ℕ
+№℗℘
+ℙℚℛℜℝ
+℞℟℠℡™℣
+ℤ
+℥
+Ω
+℧
+ℨ
+℩
+KÅℬℭ
+℮
+ℯℰℱℲℳℴℵℶℷℸℹ
+℺℻
+ℼℽℾℿ
+⅀⅁⅂⅃⅄
+ⅅⅆⅇⅈⅉ
+⅊⅋⅌⅍
+ⅎ
+⅏
+⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿↀↁↂↃↄↅↆↇↈ↉
+←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾⋿⌀⌁⌂⌃⌄⌅⌆⌇⌈⌉⌊⌋⌌⌍⌎⌏⌐⌑⌒⌓⌔⌕⌖⌗⌘⌙⌚⌛⌜⌝⌞⌟⌠⌡⌢⌣⌤⌥⌦⌧⌨〈〉⌫⌬⌭⌮⌯⌰⌱⌲⌳⌴⌵⌶⌷⌸⌹⌺⌻⌼⌽⌾⌿⍀⍁⍂⍃⍄⍅⍆⍇⍈⍉⍊⍋⍌⍍⍎⍏⍐⍑⍒⍓⍔⍕⍖⍗⍘⍙⍚⍛⍜⍝⍞⍟⍠⍡⍢⍣⍤⍥⍦⍧⍨⍩⍪⍫⍬⍭⍮⍯⍰⍱⍲⍳⍴⍵⍶⍷⍸⍹⍺⍻⍼⍽⍾⍿⎀⎁⎂⎃⎄⎅⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␠␡␢␣␤␥␦⑀⑁⑂⑃⑄⑅⑆⑇⑈⑉⑊
+①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛
+⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
+⓪⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾⓿
+─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀◁◂◃◄◅◆◇◈◉◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟⛠⛡⛣⛨⛩⛪⛫⛬⛭⛮⛯⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿✁✂✃✄✆✇✈✉✌✍✎✏✐✑✒✓✔✕✖✗✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❍❏❐❑❒❖❗❘❙❚❛❜❝❞❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵
+❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓
+➔➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➱➲➳➴➵➶➷➸➹➺➻➼➽➾⟀⟁⟂⟃⟄⟅⟆⟇⟈⟉⟊⟌⟐⟑⟒⟓⟔⟕⟖⟗⟘⟙⟚⟛⟜⟝⟞⟟⟠⟡⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫⟬⟭⟮⟯⟰⟱⟲⟳⟴⟵⟶⟷⟸⟹⟺⟻⟼⟽⟾⟿⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿⤀⤁⤂⤃⤄⤅⤆⤇⤈⤉⤊⤋⤌⤍⤎⤏⤐⤑⤒⤓⤔⤕⤖⤗⤘⤙⤚⤛⤜⤝⤞⤟⤠⤡⤢⤣⤤⤥⤦⤧⤨⤩⤪⤫⤬⤭⤮⤯⤰⤱⤲⤳⤴⤵⤶⤷⤸⤹⤺⤻⤼⤽⤾⤿⥀⥁⥂⥃⥄⥅⥆⥇⥈⥉⥊⥋⥌⥍⥎⥏⥐⥑⥒⥓⥔⥕⥖⥗⥘⥙⥚⥛⥜⥝⥞⥟⥠⥡⥢⥣⥤⥥⥦⥧⥨⥩⥪⥫⥬⥭⥮⥯⥰⥱⥲⥳⥴⥵⥶⥷⥸⥹⥺⥻⥼⥽⥾⥿⦀⦁⦂⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦎⦏⦐⦑⦒⦓⦔⦕⦖⦗⦘⦙⦚⦛⦜⦝⦞⦟⦠⦡⦢⦣⦤⦥⦦⦧⦨⦩⦪⦫⦬⦭⦮⦯⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⧄⧅⧆⧇⧈⧉⧊⧋⧌⧍⧎⧏⧐⧑⧒⧓⧔⧕⧖⧗⧘⧙⧚⧛⧜⧝⧞⧟⧠⧡⧢⧣⧤⧥⧦⧧⧨⧩⧪⧫⧬⧭⧮⧯⧰⧱⧲⧳⧴⧵⧶⧷⧸⧹⧺⧻⧼⧽⧾⧿⨀⨁⨂⨃⨄⨅⨆⨇⨈⨉⨊⨋⨌⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘⨙⨚⨛⨜⨝⨞⨟⨠⨡⨢⨣⨤⨥⨦⨧⨨⨩⨪⨫⨬⨭⨮⨯⨰⨱⨲⨳⨴⨵⨶⨷⨸⨹⨺⨻⨼⨽⨾⨿⩀⩁⩂⩃⩄⩅⩆⩇⩈⩉⩊⩋⩌⩍⩎⩏⩐⩑⩒⩓⩔⩕⩖⩗⩘⩙⩚⩛⩜⩝⩞⩟⩠⩡⩢⩣⩤⩥⩦⩧⩨⩩⩪⩫⩬⩭⩮⩯⩰⩱⩲⩳⩴⩵⩶⩷⩸⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚⪛⪜⪝⪞⪟⪠⪡⪢⪣⪤⪥⪦⪧⪨⪩⪪⪫⪬⪭⪮⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐⫑⫒⫓⫔⫕⫖⫗⫘⫙⫚⫛⫝̸⫝⫞⫟⫠⫡⫢⫣⫤⫥⫦⫧⫨⫩⫪⫫⫬⫭⫮⫯⫰⫱⫲⫳⫴⫵⫶⫷⫸⫹⫺⫻⫼⫽⫾⫿⬀⬁⬂⬃⬄⬅⬆⬇⬈⬉⬊⬋⬌⬍⬎⬏⬐⬑⬒⬓⬔⬕⬖⬗⬘⬙⬚⬛⬜⬝⬞⬟⬠⬡⬢⬣⬤⬥⬦⬧⬨⬩⬪⬫⬬⬭⬮⬯⬰⬱⬲⬳⬴⬵⬶⬷⬸⬹⬺⬻⬼⬽⬾⬿⭀⭁⭂⭃⭄⭅⭆⭇⭈⭉⭊⭋⭌⭐⭑⭒⭓⭔⭕⭖⭗⭘⭙
+ⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱠⱡⱢⱣⱤⱥⱦⱧⱨⱩⱪⱫⱬⱭⱮⱯⱰⱱⱲⱳⱴⱵⱶⱷⱸⱹⱺⱻⱼⱽⱾⱿⲀⲁⲂⲃⲄⲅⲆⲇⲈⲉⲊⲋⲌⲍⲎⲏⲐⲑⲒⲓⲔⲕⲖⲗⲘⲙⲚⲛⲜⲝⲞⲟⲠⲡⲢⲣⲤⲥⲦⲧⲨⲩⲪⲫⲬⲭⲮⲯⲰⲱⲲⲳⲴⲵⲶⲷⲸⲹⲺⲻⲼⲽⲾⲿⳀⳁⳂⳃⳄⳅⳆⳇⳈⳉⳊⳋⳌⳍⳎⳏⳐⳑⳒⳓⳔⳕⳖⳗⳘⳙⳚⳛⳜⳝⳞⳟⳠⳡⳢⳣⳤ
+⳥⳦⳧⳨⳩⳪
+ⳫⳬⳭⳮ⳯⳰⳱
+⳹⳺⳻⳼
+⳽
+⳾⳿
+ⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥⴰⴱⴲⴳⴴⴵⴶⴷⴸⴹⴺⴻⴼⴽⴾⴿⵀⵁⵂⵃⵄⵅⵆⵇⵈⵉⵊⵋⵌⵍⵎⵏⵐⵑⵒⵓⵔⵕⵖⵗⵘⵙⵚⵛⵜⵝⵞⵟⵠⵡⵢⵣⵤⵥⵯⶀⶁⶂⶃⶄⶅⶆⶇⶈⶉⶊⶋⶌⶍⶎⶏⶐⶑⶒⶓⶔⶕⶖⶠⶡⶢⶣⶤⶥⶦⶨⶩⶪⶫⶬⶭⶮⶰⶱⶲⶳⶴⶵⶶⶸⶹⶺⶻⶼⶽⶾⷀⷁⷂⷃⷄⷅⷆⷈⷉⷊⷋⷌⷍⷎⷐⷑⷒⷓⷔⷕⷖⷘⷙⷚⷛⷜⷝⷞⷠⷡⷢⷣⷤⷥⷦⷧⷨⷩⷪⷫⷬⷭⷮⷯⷰⷱⷲⷳⷴⷵⷶⷷⷸⷹⷺⷻⷼⷽⷾⷿ
+⸀⸁⸂⸃⸄⸅⸆⸇⸈⸉⸊⸋⸌⸍⸎⸏⸐⸑⸒⸓⸔⸕⸖⸗⸘⸙⸚⸛⸜⸝⸞⸟⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⸪⸫⸬⸭⸮
+ⸯ
+⸰⸱⺀⺁⺂⺃⺄⺅⺆⺇⺈⺉⺊⺋⺌⺍⺎⺏⺐⺑⺒⺓⺔⺕⺖⺗⺘⺙⺛⺜⺝⺞⺟⺠⺡⺢⺣⺤⺥⺦⺧⺨⺩⺪⺫⺬⺭⺮⺯⺰⺱⺲⺳⺴⺵⺶⺷⺸⺹⺺⺻⺼⺽⺾⺿⻀⻁⻂⻃⻄⻅⻆⻇⻈⻉⻊⻋⻌⻍⻎⻏⻐⻑⻒⻓⻔⻕⻖⻗⻘⻙⻚⻛⻜⻝⻞⻟⻠⻡⻢⻣⻤⻥⻦⻧⻨⻩⻪⻫⻬⻭⻮⻯⻰⻱⻲⻳⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⿰⿱⿲⿳⿴⿵⿶⿷⿸⿹⿺⿻ 、。〃〄
+々〆〇
+〈〉《》「」『』【】〒〓〔〕〖〗〘〙〚〛〜〝〞〟〠
+〡〢〣〤〥〦〧〨〩〪〭〮〯〫〬
+〰
+〱〲〳〴〵
+〶〷
+〸〹〺〻〼
+〽〾〿
+ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゙゚
+゛゜
+ゝゞゟ
+゠
+ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ
+・
+ーヽヾヿㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩㄪㄫㄬㄭㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣㅤㅥㅦㅧㅨㅩㅪㅫㅬㅭㅮㅯㅰㅱㅲㅳㅴㅵㅶㅷㅸㅹㅺㅻㅼㅽㅾㅿㆀㆁㆂㆃㆄㆅㆆㆇㆈㆉㆊㆋㆌㆍㆎ
+㆐㆑
+㆒㆓㆔㆕
+㆖㆗㆘㆙㆚㆛㆜㆝㆞㆟
+ㆠㆡㆢㆣㆤㆥㆦㆧㆨㆩㆪㆫㆬㆭㆮㆯㆰㆱㆲㆳㆴㆵㆶㆷ
+㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣
+ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ
+㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛㈜㈝㈞
+㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩
+㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃㉄㉅㉆㉇㉈㉉㉊㉋㉌㉍㉎㉏㉐
+㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟
+㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻㉼㉽㉾㉿
+㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉
+㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗㊘㊙㊚㊛㊜㊝㊞㊟㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰
+㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
+㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㋌㋍㋎㋏㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗㍘㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍱㍲㍳㍴㍵㍶㍷㍸㍹㍺㍻㍼㍽㍾㍿㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㎣㎤㎥㎦㎧㎨㎩㎪㎫㎬㎭㎮㎯㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㏂㏃㏄㏅㏆㏇㏈㏉㏊㏋㏌㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏘㏙㏚㏛㏜㏝㏞㏟㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㏿
+㐀䶵
+䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉䷊䷋䷌䷍䷎䷏䷐䷑䷒䷓䷔䷕䷖䷗䷘䷙䷚䷛䷜䷝䷞䷟䷠䷡䷢䷣䷤䷥䷦䷧䷨䷩䷪䷫䷬䷭䷮䷯䷰䷱䷲䷳䷴䷵䷶䷷䷸䷹䷺䷻䷼䷽䷾䷿
+一鿋ꀀꀁꀂꀃꀄꀅꀆꀇꀈꀉꀊꀋꀌꀍꀎꀏꀐꀑꀒꀓꀔꀕꀖꀗꀘꀙꀚꀛꀜꀝꀞꀟꀠꀡꀢꀣꀤꀥꀦꀧꀨꀩꀪꀫꀬꀭꀮꀯꀰꀱꀲꀳꀴꀵꀶꀷꀸꀹꀺꀻꀼꀽꀾꀿꁀꁁꁂꁃꁄꁅꁆꁇꁈꁉꁊꁋꁌꁍꁎꁏꁐꁑꁒꁓꁔꁕꁖꁗꁘꁙꁚꁛꁜꁝꁞꁟꁠꁡꁢꁣꁤꁥꁦꁧꁨꁩꁪꁫꁬꁭꁮꁯꁰꁱꁲꁳꁴꁵꁶꁷꁸꁹꁺꁻꁼꁽꁾꁿꂀꂁꂂꂃꂄꂅꂆꂇꂈꂉꂊꂋꂌꂍꂎꂏꂐꂑꂒꂓꂔꂕꂖꂗꂘꂙꂚꂛꂜꂝꂞꂟꂠꂡꂢꂣꂤꂥꂦꂧꂨꂩꂪꂫꂬꂭꂮꂯꂰꂱꂲꂳꂴꂵꂶꂷꂸꂹꂺꂻꂼꂽꂾꂿꃀꃁꃂꃃꃄꃅꃆꃇꃈꃉꃊꃋꃌꃍꃎꃏꃐꃑꃒꃓꃔꃕꃖꃗꃘꃙꃚꃛꃜꃝꃞꃟꃠꃡꃢꃣꃤꃥꃦꃧꃨꃩꃪꃫꃬꃭꃮꃯꃰꃱꃲꃳꃴꃵꃶꃷꃸꃹꃺꃻꃼꃽꃾꃿꄀꄁꄂꄃꄄꄅꄆꄇꄈꄉꄊꄋꄌꄍꄎꄏꄐꄑꄒꄓꄔꄕꄖꄗꄘꄙꄚꄛꄜꄝꄞꄟꄠꄡꄢꄣꄤꄥꄦꄧꄨꄩꄪꄫꄬꄭꄮꄯꄰꄱꄲꄳꄴꄵꄶꄷꄸꄹꄺꄻꄼꄽꄾꄿꅀꅁꅂꅃꅄꅅꅆꅇꅈꅉꅊꅋꅌꅍꅎꅏꅐꅑꅒꅓꅔꅕꅖꅗꅘꅙꅚꅛꅜꅝꅞꅟꅠꅡꅢꅣꅤꅥꅦꅧꅨꅩꅪꅫꅬꅭꅮꅯꅰꅱꅲꅳꅴꅵꅶꅷꅸꅹꅺꅻꅼꅽꅾꅿꆀꆁꆂꆃꆄꆅꆆꆇꆈꆉꆊꆋꆌꆍꆎꆏꆐꆑꆒꆓꆔꆕꆖꆗꆘꆙꆚꆛꆜꆝꆞꆟꆠꆡꆢꆣꆤꆥꆦꆧꆨꆩꆪꆫꆬꆭꆮꆯꆰꆱꆲꆳꆴꆵꆶꆷꆸꆹꆺꆻꆼꆽꆾꆿꇀꇁꇂꇃꇄꇅꇆꇇꇈꇉꇊꇋꇌꇍꇎꇏꇐꇑꇒꇓꇔꇕꇖꇗꇘꇙꇚꇛꇜꇝꇞꇟꇠꇡꇢꇣꇤꇥꇦꇧꇨꇩꇪꇫꇬꇭꇮꇯꇰꇱꇲꇳꇴꇵꇶꇷꇸꇹꇺꇻꇼꇽꇾꇿꈀꈁꈂꈃꈄꈅꈆꈇꈈꈉꈊꈋꈌꈍꈎꈏꈐꈑꈒꈓꈔꈕꈖꈗꈘꈙꈚꈛꈜꈝꈞꈟꈠꈡꈢꈣꈤꈥꈦꈧꈨꈩꈪꈫꈬꈭꈮꈯꈰꈱꈲꈳꈴꈵꈶꈷꈸꈹꈺꈻꈼꈽꈾꈿꉀꉁꉂꉃꉄꉅꉆꉇꉈꉉꉊꉋꉌꉍꉎꉏꉐꉑꉒꉓꉔꉕꉖꉗꉘꉙꉚꉛꉜꉝꉞꉟꉠꉡꉢꉣꉤꉥꉦꉧꉨꉩꉪꉫꉬꉭꉮꉯꉰꉱꉲꉳꉴꉵꉶꉷꉸꉹꉺꉻꉼꉽꉾꉿꊀꊁꊂꊃꊄꊅꊆꊇꊈꊉꊊꊋꊌꊍꊎꊏꊐꊑꊒꊓꊔꊕꊖꊗꊘꊙꊚꊛꊜꊝꊞꊟꊠꊡꊢꊣꊤꊥꊦꊧꊨꊩꊪꊫꊬꊭꊮꊯꊰꊱꊲꊳꊴꊵꊶꊷꊸꊹꊺꊻꊼꊽꊾꊿꋀꋁꋂꋃꋄꋅꋆꋇꋈꋉꋊꋋꋌꋍꋎꋏꋐꋑꋒꋓꋔꋕꋖꋗꋘꋙꋚꋛꋜꋝꋞꋟꋠꋡꋢꋣꋤꋥꋦꋧꋨꋩꋪꋫꋬꋭꋮꋯꋰꋱꋲꋳꋴꋵꋶꋷꋸꋹꋺꋻꋼꋽꋾꋿꌀꌁꌂꌃꌄꌅꌆꌇꌈꌉꌊꌋꌌꌍꌎꌏꌐꌑꌒꌓꌔꌕꌖꌗꌘꌙꌚꌛꌜꌝꌞꌟꌠꌡꌢꌣꌤꌥꌦꌧꌨꌩꌪꌫꌬꌭꌮꌯꌰꌱꌲꌳꌴꌵꌶꌷꌸꌹꌺꌻꌼꌽꌾꌿꍀꍁꍂꍃꍄꍅꍆꍇꍈꍉꍊꍋꍌꍍꍎꍏꍐꍑꍒꍓꍔꍕꍖꍗꍘꍙꍚꍛꍜꍝꍞꍟꍠꍡꍢꍣꍤꍥꍦꍧꍨꍩꍪꍫꍬꍭꍮꍯꍰꍱꍲꍳꍴꍵꍶꍷꍸꍹꍺꍻꍼꍽꍾꍿꎀꎁꎂꎃꎄꎅꎆꎇꎈꎉꎊꎋꎌꎍꎎꎏꎐꎑꎒꎓꎔꎕꎖꎗꎘꎙꎚꎛꎜꎝꎞꎟꎠꎡꎢꎣꎤꎥꎦꎧꎨꎩꎪꎫꎬꎭꎮꎯꎰꎱꎲꎳꎴꎵꎶꎷꎸꎹꎺꎻꎼꎽꎾꎿꏀꏁꏂꏃꏄꏅꏆꏇꏈꏉꏊꏋꏌꏍꏎꏏꏐꏑꏒꏓꏔꏕꏖꏗꏘꏙꏚꏛꏜꏝꏞꏟꏠꏡꏢꏣꏤꏥꏦꏧꏨꏩꏪꏫꏬꏭꏮꏯꏰꏱꏲꏳꏴꏵꏶꏷꏸꏹꏺꏻꏼꏽꏾꏿꐀꐁꐂꐃꐄꐅꐆꐇꐈꐉꐊꐋꐌꐍꐎꐏꐐꐑꐒꐓꐔꐕꐖꐗꐘꐙꐚꐛꐜꐝꐞꐟꐠꐡꐢꐣꐤꐥꐦꐧꐨꐩꐪꐫꐬꐭꐮꐯꐰꐱꐲꐳꐴꐵꐶꐷꐸꐹꐺꐻꐼꐽꐾꐿꑀꑁꑂꑃꑄꑅꑆꑇꑈꑉꑊꑋꑌꑍꑎꑏꑐꑑꑒꑓꑔꑕꑖꑗꑘꑙꑚꑛꑜꑝꑞꑟꑠꑡꑢꑣꑤꑥꑦꑧꑨꑩꑪꑫꑬꑭꑮꑯꑰꑱꑲꑳꑴꑵꑶꑷꑸꑹꑺꑻꑼꑽꑾꑿꒀꒁꒂꒃꒄꒅꒆꒇꒈꒉꒊꒋꒌ
+꒐꒑꒒꒓꒔꒕꒖꒗꒘꒙꒚꒛꒜꒝꒞꒟꒠꒡꒢꒣꒤꒥꒦꒧꒨꒩꒪꒫꒬꒭꒮꒯꒰꒱꒲꒳꒴꒵꒶꒷꒸꒹꒺꒻꒼꒽꒾꒿꓀꓁꓂꓃꓄꓅꓆
+ꓐꓑꓒꓓꓔꓕꓖꓗꓘꓙꓚꓛꓜꓝꓞꓟꓠꓡꓢꓣꓤꓥꓦꓧꓨꓩꓪꓫꓬꓭꓮꓯꓰꓱꓲꓳꓴꓵꓶꓷꓸꓹꓺꓻꓼꓽ
+꓾꓿
+ꔀꔁꔂꔃꔄꔅꔆꔇꔈꔉꔊꔋꔌꔍꔎꔏꔐꔑꔒꔓꔔꔕꔖꔗꔘꔙꔚꔛꔜꔝꔞꔟꔠꔡꔢꔣꔤꔥꔦꔧꔨꔩꔪꔫꔬꔭꔮꔯꔰꔱꔲꔳꔴꔵꔶꔷꔸꔹꔺꔻꔼꔽꔾꔿꕀꕁꕂꕃꕄꕅꕆꕇꕈꕉꕊꕋꕌꕍꕎꕏꕐꕑꕒꕓꕔꕕꕖꕗꕘꕙꕚꕛꕜꕝꕞꕟꕠꕡꕢꕣꕤꕥꕦꕧꕨꕩꕪꕫꕬꕭꕮꕯꕰꕱꕲꕳꕴꕵꕶꕷꕸꕹꕺꕻꕼꕽꕾꕿꖀꖁꖂꖃꖄꖅꖆꖇꖈꖉꖊꖋꖌꖍꖎꖏꖐꖑꖒꖓꖔꖕꖖꖗꖘꖙꖚꖛꖜꖝꖞꖟꖠꖡꖢꖣꖤꖥꖦꖧꖨꖩꖪꖫꖬꖭꖮꖯꖰꖱꖲꖳꖴꖵꖶꖷꖸꖹꖺꖻꖼꖽꖾꖿꗀꗁꗂꗃꗄꗅꗆꗇꗈꗉꗊꗋꗌꗍꗎꗏꗐꗑꗒꗓꗔꗕꗖꗗꗘꗙꗚꗛꗜꗝꗞꗟꗠꗡꗢꗣꗤꗥꗦꗧꗨꗩꗪꗫꗬꗭꗮꗯꗰꗱꗲꗳꗴꗵꗶꗷꗸꗹꗺꗻꗼꗽꗾꗿꘀꘁꘂꘃꘄꘅꘆꘇꘈꘉꘊꘋꘌ
+꘍꘎꘏
+ꘐꘑꘒꘓꘔꘕꘖꘗꘘꘙꘚꘛꘜꘝꘞꘟ꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩ꘪꘫꙀꙁꙂꙃꙄꙅꙆꙇꙈꙉꙊꙋꙌꙍꙎꙏꙐꙑꙒꙓꙔꙕꙖꙗꙘꙙꙚꙛꙜꙝꙞꙟꙢꙣꙤꙥꙦꙧꙨꙩꙪꙫꙬꙭꙮ꙯꙰꙱꙲
+꙳
+꙼꙽
+꙾
+ꙿꚀꚁꚂꚃꚄꚅꚆꚇꚈꚉꚊꚋꚌꚍꚎꚏꚐꚑꚒꚓꚔꚕꚖꚗꚠꚡꚢꚣꚤꚥꚦꚧꚨꚩꚪꚫꚬꚭꚮꚯꚰꚱꚲꚳꚴꚵꚶꚷꚸꚹꚺꚻꚼꚽꚾꚿꛀꛁꛂꛃꛄꛅꛆꛇꛈꛉꛊꛋꛌꛍꛎꛏꛐꛑꛒꛓꛔꛕꛖꛗꛘꛙꛚꛛꛜꛝꛞꛟꛠꛡꛢꛣꛤꛥꛦꛧꛨꛩꛪꛫꛬꛭꛮꛯ꛰꛱
+꛲꛳꛴꛵꛶꛷꜀꜁꜂꜃꜄꜅꜆꜇꜈꜉꜊꜋꜌꜍꜎꜏꜐꜑꜒꜓꜔꜕꜖
+ꜗꜘꜙꜚꜛꜜꜝꜞꜟ
+꜠꜡
+ꜢꜣꜤꜥꜦꜧꜨꜩꜪꜫꜬꜭꜮꜯꜰꜱꜲꜳꜴꜵꜶꜷꜸꜹꜺꜻꜼꜽꜾꜿꝀꝁꝂꝃꝄꝅꝆꝇꝈꝉꝊꝋꝌꝍꝎꝏꝐꝑꝒꝓꝔꝕꝖꝗꝘꝙꝚꝛꝜꝝꝞꝟꝠꝡꝢꝣꝤꝥꝦꝧꝨꝩꝪꝫꝬꝭꝮꝯꝰꝱꝲꝳꝴꝵꝶꝷꝸꝹꝺꝻꝼꝽꝾꝿꞀꞁꞂꞃꞄꞅꞆꞇꞈ
+꞉꞊
+Ꞌꞌꟻꟼꟽꟾꟿꠀꠁꠂꠃꠄꠅ꠆ꠇꠈꠉꠊꠋꠌꠍꠎꠏꠐꠑꠒꠓꠔꠕꠖꠗꠘꠙꠚꠛꠜꠝꠞꠟꠠꠡꠢꠣꠤꠥꠦꠧ
+꠨꠩꠪꠫
+꠰꠱꠲꠳꠴꠵
+꠶꠷꠸꠹
+ꡀꡁꡂꡃꡄꡅꡆꡇꡈꡉꡊꡋꡌꡍꡎꡏꡐꡑꡒꡓꡔꡕꡖꡗꡘꡙꡚꡛꡜꡝꡞꡟꡠꡡꡢꡣꡤꡥꡦꡧꡨꡩꡪꡫꡬꡭꡮꡯꡰꡱꡲꡳ
+꡴꡵꡶꡷
+ꢀꢁꢂꢃꢄꢅꢆꢇꢈꢉꢊꢋꢌꢍꢎꢏꢐꢑꢒꢓꢔꢕꢖꢗꢘꢙꢚꢛꢜꢝꢞꢟꢠꢡꢢꢣꢤꢥꢦꢧꢨꢩꢪꢫꢬꢭꢮꢯꢰꢱꢲꢳꢴꢵꢶꢷꢸꢹꢺꢻꢼꢽꢾꢿꣀꣁꣂꣃ꣄
+꣎꣏
+꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꣠꣡꣢꣣꣤꣥꣦꣧꣨꣩꣪꣫꣬꣭꣮꣯꣰꣱ꣲꣳꣴꣵꣶꣷ
+꣸꣹꣺
+ꣻ꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉ꤊꤋꤌꤍꤎꤏꤐꤑꤒꤓꤔꤕꤖꤗꤘꤙꤚꤛꤜꤝꤞꤟꤠꤡꤢꤣꤤꤥꤦꤧꤨꤩꤪ꤫꤬꤭
+꤮꤯
+ꤰꤱꤲꤳꤴꤵꤶꤷꤸꤹꤺꤻꤼꤽꤾꤿꥀꥁꥂꥃꥄꥅꥆꥇꥈꥉꥊꥋꥌꥍꥎꥏꥐꥑꥒ꥓
+꥟
+ꥠꥡꥢꥣꥤꥥꥦꥧꥨꥩꥪꥫꥬꥭꥮꥯꥰꥱꥲꥳꥴꥵꥶꥷꥸꥹꥺꥻꥼꦀꦁꦂꦃꦄꦅꦆꦇꦈꦉꦊꦋꦌꦍꦎꦏꦐꦑꦒꦓꦔꦕꦖꦗꦘꦙꦚꦛꦜꦝꦞꦟꦠꦡꦢꦣꦤꦥꦦꦧꦨꦩꦪꦫꦬꦭꦮꦯꦰꦱꦲ꦳ꦴꦵꦶꦷꦸꦹꦺꦻꦼꦽꦾꦿ꧀
+꧁꧂꧃꧄꧅꧆꧇꧈꧉꧊꧋꧌꧍
+ꧏ꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙
+꧞꧟
+ꨀꨁꨂꨃꨄꨅꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨳꨴꨵꨶꩀꩁꩂꩃꩄꩅꩆꩇꩈꩉꩊꩋꩌꩍ꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙
+꩜꩝꩞꩟
+ꩠꩡꩢꩣꩤꩥꩦꩧꩨꩩꩪꩫꩬꩭꩮꩯꩰꩱꩲꩳꩴꩵꩶ
+꩷꩸꩹
+ꩺꩻꪀꪁꪂꪃꪄꪅꪆꪇꪈꪉꪊꪋꪌꪍꪎꪏꪐꪑꪒꪓꪔꪕꪖꪗꪘꪙꪚꪛꪜꪝꪞꪟꪠꪡꪢꪣꪤꪥꪦꪧꪨꪩꪪꪫꪬꪭꪮꪯꪰꪱꪴꪲꪳꪵꪶꪷꪸꪹꪺꪻꪼꪽꪾ꪿ꫀ꫁ꫂꫛꫜꫝ
+꫞꫟
+ꯀꯁꯂꯃꯄꯅꯆꯇꯈꯉꯊꯋꯌꯍꯎꯏꯐꯑꯒꯓꯔꯕꯖꯗꯘꯙꯚꯛꯜꯝꯞꯟꯠꯡꯢꯣꯤꯥꯦꯧꯨꯩꯪ
+꯫
+꯬꯭꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹가힣ힰힱힲힳힴힵힶힷힸힹힺힻힼힽힾힿퟀퟁퟂퟃퟄퟅퟆퟋퟌퟍퟎퟏퟐퟑퟒퟓퟔퟕퟖퟗퟘퟙퟚퟛퟜퟝퟞퟟퟠퟡퟢퟣퟤퟥퟦퟧퟨퟩퟪퟫퟬퟭퟮퟯퟰퟱퟲퟳퟴퟵퟶퟷퟸퟹퟺퟻ
+
+豈更車賈滑串句龜龜契金喇奈懶癩羅蘿螺裸邏樂洛烙珞落酪駱亂卵欄爛蘭鸞嵐濫藍襤拉臘蠟廊朗浪狼郎來冷勞擄櫓爐盧老蘆虜路露魯鷺碌祿綠菉錄鹿論壟弄籠聾牢磊賂雷壘屢樓淚漏累縷陋勒肋凜凌稜綾菱陵讀拏樂諾丹寧怒率異北磻便復不泌數索參塞省葉說殺辰沈拾若掠略亮兩凉梁糧良諒量勵呂女廬旅濾礪閭驪麗黎力曆歷轢年憐戀撚漣煉璉秊練聯輦蓮連鍊列劣咽烈裂說廉念捻殮簾獵令囹寧嶺怜玲瑩羚聆鈴零靈領例禮醴隸惡了僚寮尿料樂燎療蓼遼龍暈阮劉杻柳流溜琉留硫紐類六戮陸倫崙淪輪律慄栗率隆利吏履易李梨泥理痢罹裏裡里離匿溺吝燐璘藺隣鱗麟林淋臨立笠粒狀炙識什茶刺切度拓糖宅洞暴輻行降見廓兀嗀﨎﨏塚﨑晴﨓﨔凞猪益礼神祥福靖精羽﨟蘒﨡諸﨣﨤逸都﨧﨨﨩飯飼館鶴侮僧免勉勤卑喝嘆器塀墨層屮悔慨憎懲敏既暑梅海渚漢煮爫琢碑社祉祈祐祖祝禍禎穀突節練縉繁署者臭艹艹著褐視謁謹賓贈辶逸難響頻恵𤋮舘並况全侀充冀勇勺喝啕喙嗢塚墳奄奔婢嬨廒廙彩徭惘慎愈憎慠懲戴揄搜摒敖晴朗望杖歹殺流滛滋漢瀞煮瞧爵犯猪瑱甆画瘝瘟益盛直睊着磌窱節类絛練缾者荒華蝹襁覆視調諸請謁諾諭謹變贈輸遲醙鉶陼難靖韛響頋頻鬒龜𢡊𢡄𣏕㮝䀘䀹𥉉𥳐𧻓齃龎fffiflffifflſtstﬓﬔﬕﬖﬗיִﬞײַﬠﬡﬢﬣﬤﬥﬦﬧﬨ
+﬩
+שׁשׂשּׁשּׂאַאָאּבּגּדּהּוּזּטּיּךּכּלּמּנּסּףּפּצּקּרּשּתּוֹבֿכֿפֿﭏﭐﭑﭒﭓﭔﭕﭖﭗﭘﭙﭚﭛﭜﭝﭞﭟﭠﭡﭢﭣﭤﭥﭦﭧﭨﭩﭪﭫﭬﭭﭮﭯﭰﭱﭲﭳﭴﭵﭶﭷﭸﭹﭺﭻﭼﭽﭾﭿﮀﮁﮂﮃﮄﮅﮆﮇﮈﮉﮊﮋﮌﮍﮎﮏﮐﮑﮒﮓﮔﮕﮖﮗﮘﮙﮚﮛﮜﮝﮞﮟﮠﮡﮢﮣﮤﮥﮦﮧﮨﮩﮪﮫﮬﮭﮮﮯﮰﮱﯓﯔﯕﯖﯗﯘﯙﯚﯛﯜﯝﯞﯟﯠﯡﯢﯣﯤﯥﯦﯧﯨﯩﯪﯫﯬﯭﯮﯯﯰﯱﯲﯳﯴﯵﯶﯷﯸﯹﯺﯻﯼﯽﯾﯿﰀﰁﰂﰃﰄﰅﰆﰇﰈﰉﰊﰋﰌﰍﰎﰏﰐﰑﰒﰓﰔﰕﰖﰗﰘﰙﰚﰛﰜﰝﰞﰟﰠﰡﰢﰣﰤﰥﰦﰧﰨﰩﰪﰫﰬﰭﰮﰯﰰﰱﰲﰳﰴﰵﰶﰷﰸﰹﰺﰻﰼﰽﰾﰿﱀﱁﱂﱃﱄﱅﱆﱇﱈﱉﱊﱋﱌﱍﱎﱏﱐﱑﱒﱓﱔﱕﱖﱗﱘﱙﱚﱛﱜﱝﱞﱟﱠﱡﱢﱣﱤﱥﱦﱧﱨﱩﱪﱫﱬﱭﱮﱯﱰﱱﱲﱳﱴﱵﱶﱷﱸﱹﱺﱻﱼﱽﱾﱿﲀﲁﲂﲃﲄﲅﲆﲇﲈﲉﲊﲋﲌﲍﲎﲏﲐﲑﲒﲓﲔﲕﲖﲗﲘﲙﲚﲛﲜﲝﲞﲟﲠﲡﲢﲣﲤﲥﲦﲧﲨﲩﲪﲫﲬﲭﲮﲯﲰﲱﲲﲳﲴﲵﲶﲷﲸﲹﲺﲻﲼﲽﲾﲿﳀﳁﳂﳃﳄﳅﳆﳇﳈﳉﳊﳋﳌﳍﳎﳏﳐﳑﳒﳓﳔﳕﳖﳗﳘﳙﳚﳛﳜﳝﳞﳟﳠﳡﳢﳣﳤﳥﳦﳧﳨﳩﳪﳫﳬﳭﳮﳯﳰﳱﳲﳳﳴﳵﳶﳷﳸﳹﳺﳻﳼﳽﳾﳿﴀﴁﴂﴃﴄﴅﴆﴇﴈﴉﴊﴋﴌﴍﴎﴏﴐﴑﴒﴓﴔﴕﴖﴗﴘﴙﴚﴛﴜﴝﴞﴟﴠﴡﴢﴣﴤﴥﴦﴧﴨﴩﴪﴫﴬﴭﴮﴯﴰﴱﴲﴳﴴﴵﴶﴷﴸﴹﴺﴻﴼﴽ
+﴾﴿
+ﵐﵑﵒﵓﵔﵕﵖﵗﵘﵙﵚﵛﵜﵝﵞﵟﵠﵡﵢﵣﵤﵥﵦﵧﵨﵩﵪﵫﵬﵭﵮﵯﵰﵱﵲﵳﵴﵵﵶﵷﵸﵹﵺﵻﵼﵽﵾﵿﶀﶁﶂﶃﶄﶅﶆﶇﶈﶉﶊﶋﶌﶍﶎﶏﶒﶓﶔﶕﶖﶗﶘﶙﶚﶛﶜﶝﶞﶟﶠﶡﶢﶣﶤﶥﶦﶧﶨﶩﶪﶫﶬﶭﶮﶯﶰﶱﶲﶳﶴﶵﶶﶷﶸﶹﶺﶻﶼﶽﶾﶿﷀﷁﷂﷃﷄﷅﷆﷇﷰﷱﷲﷳﷴﷵﷶﷷﷸﷹﷺﷻ
+﷼﷽
+︀︁︂︃︄︅︆︇︈︉︊︋︌︍︎️
+︐︑︒︓︔︕︖︗︘︙
+︠︡︢︣︤︥︦
+︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹢﹣﹤﹥﹦﹨﹩﹪﹫
+ﹰﹱﹲﹳﹴﹶﹷﹸﹹﹺﹻﹼﹽﹾﹿﺀﺁﺂﺃﺄﺅﺆﺇﺈﺉﺊﺋﺌﺍﺎﺏﺐﺑﺒﺓﺔﺕﺖﺗﺘﺙﺚﺛﺜﺝﺞﺟﺠﺡﺢﺣﺤﺥﺦﺧﺨﺩﺪﺫﺬﺭﺮﺯﺰﺱﺲﺳﺴﺵﺶﺷﺸﺹﺺﺻﺼﺽﺾﺿﻀﻁﻂﻃﻄﻅﻆﻇﻈﻉﻊﻋﻌﻍﻎﻏﻐﻑﻒﻓﻔﻕﻖﻗﻘﻙﻚﻛﻜﻝﻞﻟﻠﻡﻢﻣﻤﻥﻦﻧﻨﻩﻪﻫﻬﻭﻮﻯﻰﻱﻲﻳﻴﻵﻶﻷﻸﻹﻺﻻﻼ
+!"#$%&'()*+,-./
+0123456789
+:;<=>?@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ
+[\]^_`
+abcdefghijklmnopqrstuvwxyz
+{|}~⦅⦆。「」、・
+ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ᅠᄀᄁᆪᄂᆬᆭᄃᄄᄅᆰᆱᆲᆳᆴᆵᄚᄆᄇᄈᄡᄉᄊᄋᄌᄍᄎᄏᄐᄑ하ᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵ
+¢£¬ ̄¦¥₩│←↑→↓■○�
+𐀀 \ No newline at end of file
diff --git a/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.info b/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.info
new file mode 100644
index 0000000..6413e94
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.info
@@ -0,0 +1,12 @@
+name = "Search embedded form"
+description = "Support module for search module testing of embedded forms."
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.module b/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.module
new file mode 100644
index 0000000..4845796
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/tests/search_embedded_form.module
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Test module implementing a form that can be embedded in search results.
+ *
+ * Embedded form are important, for example, for ecommerce sites where each
+ * search result may included an embedded form with buttons like "Add to cart"
+ * for each individual product (node) listed in the search results.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function search_embedded_form_menu() {
+ $items['search_embedded_form'] = array(
+ 'title' => 'Search_Embed_Form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('search_embedded_form_form'),
+ 'access arguments' => array('search content'),
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Builds a form for embedding in search results for testing.
+ *
+ * @see search_embedded_form_form_submit().
+ */
+function search_embedded_form_form($form, &$form_state) {
+ $count = variable_get('search_embedded_form_submitted', 0);
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Your name'),
+ '#maxlength' => 255,
+ '#default_value' => '',
+ '#required' => TRUE,
+ '#description' => t('Times form has been submitted: %count', array('%count' => $count)),
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Send away'),
+ );
+
+ $form['#submit'][] = 'search_embedded_form_form_submit';
+
+ return $form;
+}
+
+/**
+ * Submit handler for search_embedded_form_form().
+ */
+function search_embedded_form_form_submit($form, &$form_state) {
+ $count = variable_get('search_embedded_form_submitted', 0) + 1;
+ variable_set('search_embedded_form_submitted', $count);
+ drupal_set_message(t('Test form was submitted'));
+}
+
+/**
+ * Adds the test form to search results.
+ */
+function search_embedded_form_preprocess_search_result(&$variables) {
+ $form = drupal_get_form('search_embedded_form_form');
+ $variables['snippet'] .= drupal_render($form);
+}
diff --git a/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.info b/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.info
new file mode 100644
index 0000000..d4296c8
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.info
@@ -0,0 +1,12 @@
+name = "Test search type"
+description = "Support module for search module testing."
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.module b/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.module
new file mode 100644
index 0000000..80c050c
--- /dev/null
+++ b/kolab.org/www/drupal-7.26/modules/search/tests/search_extra_type.module
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Dummy module implementing a search type for search module testing.
+ */
+
+/**
+ * Implements hook_search_info().
+ */
+function search_extra_type_search_info() {
+ return array(
+ 'title' => 'Dummy search type',
+ 'path' => 'dummy_path',
+ 'conditions_callback' => 'search_extra_type_conditions',
+ );
+}
+
+/**
+ * Test conditions callback for hook_search_info().
+ */
+function search_extra_type_conditions() {
+ $conditions = array();
+
+ if (!empty($_REQUEST['search_conditions'])) {
+ $conditions['search_conditions'] = $_REQUEST['search_conditions'];
+ }
+ return $conditions;
+}
+
+/**
+ * Implements hook_search_execute().
+ *
+ * This is a dummy search, so when search "executes", we just return a dummy
+ * result containing the keywords and a list of conditions.
+ */
+function search_extra_type_search_execute($keys = NULL, $conditions = NULL) {
+ if (!$keys) {
+ $keys = '';
+ }
+ return array(
+ array(
+ 'link' => url('node'),
+ 'type' => 'Dummy result type',
+ 'title' => 'Dummy title',
+ 'snippet' => "Dummy search snippet to display. Keywords: {$keys}\n\nConditions: " . print_r($conditions, TRUE),
+ ),
+ );
+}
+
+/**
+ * Implements hook_search_page().
+ *
+ * Adds some text to the search page so we can verify that it runs.
+ */
+function search_extra_type_search_page($results) {
+ $output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
+
+ foreach ($results as $entry) {
+ $output[] = array(
+ '#theme' => 'search_result',
+ '#result' => $entry,
+ '#module' => 'search_extra_type',
+ );
+ }
+ $output['suffix']['#markup'] = '</ol>' . theme('pager');
+
+ return $output;
+}