-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfaceted_search.module
executable file
·530 lines (490 loc) · 17.5 KB
/
faceted_search.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
<?php
/**
* @file
* An API for performing faceted searches.
*/
require_once('./'. drupal_get_path('module', 'faceted_search') .'/faceted_search.inc');
/**
* Implementation of hook_help().
*/
function faceted_search_help($path, $arg) {
switch ($path) {
case 'admin/help#faceted_search':
return '<p>'. t('A faceted search interface allows users to browse content in such a way that they can rapidly get acquainted with the scope and nature of the content without ever feeling lost. Such system relies on metadata (such as !categories) usually built specifically for !classification.', array('!categories' => l(t('categories'), 'admin/help/taxonomy'), '!classification' => l(t('faceted classification'), 'http://en.wikipedia.org/wiki/Faceted_classification'))) .'</p><p>'. t('Introductory information is provided in !article about when to use — and how to build — a faceted classification.', array('!article' => l(t('this article'), 'http://www.miskatonic.org/library/facet-web-howto.html'))) .'</p>';
}
}
/**
* Implementation of hook_perm().
*/
function faceted_search_perm() {
return array('administer faceted search');
}
/**
* Implementation of hook_menu().
*/
function faceted_search_menu() {
$items = array();
$items['admin/settings/faceted_search'] = array(
'title' => 'Faceted search',
'page callback' => 'faceted_search_list_page',
'access callback' => 'user_access',
'access arguments' => array('administer faceted search'),
'description' => 'Administer faceted search environments.',
'type' => MENU_NORMAL_ITEM,
'file' => 'faceted_search.admin.inc',
);
$items['admin/settings/faceted_search/list'] = array(
'title' => 'List',
'weight' => -10,
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/settings/faceted_search/add'] = array(
'title' => 'Add environment',
'page callback' => 'drupal_get_form',
'page arguments' => array('faceted_search_edit_form'),
'access callback' => 'user_access',
'access arguments' => array('administer faceted search'),
'type' => MENU_LOCAL_TASK,
'file' => 'faceted_search.admin.inc',
);
$items['admin/settings/faceted_search/delete/%faceted_search_env'] = array(
'load arguments' => array(5),
'page callback' => 'drupal_get_form',
'page arguments' => array('faceted_search_delete_form', 4),
'access arguments' => array('administer faceted search'),
'type' => MENU_CALLBACK,
'file' => 'faceted_search.admin.inc',
);
$items['admin/settings/faceted_search/%faceted_search_env'] = array(
'load arguments' => array(4),
'page callback' => 'drupal_get_form',
'page arguments' => array('faceted_search_edit_form', 3),
'access arguments' => array('administer faceted search'),
'type' => MENU_CALLBACK,
'file' => 'faceted_search.admin.inc',
);
return $items;
}
/**
* Similar to hook_faceted_search_collect(), this function collects the node
* keyword filters only and is called separately. Those filters allow searching
* keywords on the full nodes index.
*
* These are handled separately from other filters because they do not use a key
* in the search text and must therefore be processed after all other filters.
*
* @param $filters
* Array of filters into which this function should append new filters.
* @param $domain
* The domain where to look for filters. Possible values:
* - 'keyword filters': All possible keyword filters.
* - 'text': Filters specified in the specified search text.
* @param $env
* Id of the environment for which filters are collected. This is NULL in the
* case of a new environment.
* @param $text
* The search text. Only used when the domain is 'text'.
*/
function faceted_search_collect_node_keyword_filters(&$filters, $domain, $env, $text = '') {
switch ($domain) {
case 'keyword filters':
$filter = new faceted_search_keyword_filter('node', t('Anywhere'));
$filter->set_weight(-999); // Default weight.
$filter->set_status(TRUE); // Default status.
$filters[] = $filter;
break;
case 'text':
$keys = faceted_search_parse_keywords($text);
// Create the filters.
foreach ($keys['positive'] as $keyword) {
if (is_array($keyword)) {
$filter = new faceted_search_keyword_filter('node', '', new faceted_search_keyword_or_category($keyword));
}
elseif (strpos($keyword, ' ')) {
$filter = new faceted_search_keyword_filter('node', '', new faceted_search_keyword_phrase_category($keyword));
}
else {
$filter = new faceted_search_keyword_filter('node', '', new faceted_search_keyword_and_category($keyword));
}
$filter->set_weight(-999); // Default weight.
$filter->set_status(TRUE); // Default status.
$filters[] = $filter;
}
foreach ($keys['negative'] as $keyword) {
$filter = new faceted_search_keyword_filter('node', '', new faceted_search_keyword_not_category($keyword));
$filter->set_weight(-999); // Default weight.
$filter->set_status(TRUE); // Default status.
$filters[] = $filter;
}
}
}
/**
* Return the collection of node types handled by a given environment.
*
* @return
* Array with type names. Empty when all types are allowed in the environment.
*/
function faceted_search_types($env) {
$all_types = array();
foreach (array_keys(node_get_types('names')) as $type) {
$all_types[$type] = $type;
}
$types = array_filter($env->settings['types']);
if (!empty($types)) {
// Only return types that still exist.
$types = array_intersect($types, $all_types);
if (count($types) == count($all_types)) {
// All types are selected in the environment; do the same as if none was.
$types = array();
}
}
return $types;
}
/**
* Implementation of hook_locale().
*/
function faceted_search_locale($op = 'groups', $group = NULL) {
switch ($op) {
case 'groups':
return array('faceted_search' => t('Faceted Search'));
case 'refresh':
if ($group == 'faceted_search') {
foreach (faceted_search_get_env_ids() as $env_id) {
$env = faceted_search_env_load($env_id);
faceted_search_env_locale_refresh($env);
}
}
}
}
/**
* Refresh the localized strings of an environment.
*/
function faceted_search_env_locale_refresh($env) {
if (module_exists('i18nstrings')) {
i18nstrings_update_string('faceted_search:'. $env->env_id .':title', $env->settings['title']);
}
}
/**
* Delete the localized strings of an environment.
*/
function faceted_search_env_locale_delete($env_id) {
if (module_exists('i18nstrings')) {
i18nstrings_remove_string('faceted_search:'. $env_id .':title');
}
}
/**
* Localize the name of a node type.
*/
function faceted_search_localize_type($type, &$name) {
if (module_exists('i18ncontent')) {
$name = tt("nodetype:type:$type:name", $name);
}
}
/**
* Localize the names of all node types in a given array.
*/
function faceted_search_localize_types(&$types) {
if (module_exists('i18ncontent')) {
foreach ($types as $type => $name) {
$types[$type] = tt("nodetype:type:$type:name", $name);
}
}
}
/**
* Load a search environment from the database, or from memory if its was
* already loaded.
*
* This function also acts as an argument loader for the menu system.
*/
function faceted_search_env_load($env_id) {
static $env = NULL;
if (!is_numeric($env_id)) {
return FALSE;
}
if (!isset($env[$env_id])) {
$results = db_query('SELECT * FROM {faceted_search_env} WHERE env_id = %d', $env_id);
if ($record = db_fetch_object($results)) {
$env[$env_id] = new faceted_search($record);
}
}
return isset($env[$env_id]) ? $env[$env_id] : FALSE;
}
/**
* Return the ids of all existing environments.
*/
function faceted_search_get_env_ids() {
static $env_ids = array();
if (!$env_ids) {
$results = db_query('SELECT env_id FROM {faceted_search_env}');
while ($result = db_fetch_object($results)) {
$env_ids[$result->env_id] = $result->env_id;
}
}
return $env_ids;
}
/**
* Load filter settings into an array.
*
* @param $env
* Environment whose filters should be loaded.
* @param $include_disabled
* Optional. When FALSE, only retrieve the settings of filters that are enabled. When
* TRUE, retrieve all settings. Defaults to FALSE.
* @param $filter_key
* Optional. Filter key to load the settings for. When not set, settings are
* retrieved for all filters.
* @return
* Array of filters settings keyed by filter key and filter id.
*/
function faceted_search_load_filter_settings($env, $include_disabled = FALSE, $filter_key = NULL) {
$filter_settings = array();
if ($env->env_id) {
$where_status = $include_disabled ? '' : 'AND status = 1';
$where_filter_key = isset($filter_key) ? "AND filter_key = '%s'" : '';
$results = db_query("SELECT * FROM {faceted_search_filters} WHERE env_id = %d $where_status $where_filter_key", $env->env_id, $filter_key);
while ($settings = db_fetch_array($results)) {
$filter_settings[$settings['filter_key']][$settings['filter_id']] = $settings;
}
}
return $filter_settings;
}
/**
* Save filter settings.
*
* @param $env_id
* Environment whose filter settings are to be saved.
* @param $filter_settings
* Array where each element is itself an array of settings for an individual
* filter.
*/
function faceted_search_save_filter_settings($env_id, $filter_settings) {
db_lock_table('faceted_search_filters');
db_query('DELETE FROM {faceted_search_filters} WHERE env_id = %d', $env_id);
foreach ($filter_settings as $settings) {
db_query("INSERT INTO {faceted_search_filters} (env_id, filter_key, filter_id, status, weight, sort, max_categories) VALUES (%d, '%s', '%s', %d, %d, '%s', %d)", $env_id, $settings['filter_key'], $settings['filter_id'], $settings['status'], $settings['weight'], isset($settings['sort']) ? $settings['sort'] : '', isset($settings['max_categories']) ? $settings['max_categories'] : 0);
}
db_unlock_tables();
}
/**
* Return a selection with all the filters from the given filter settings.
*
* The selection is an array keyed by filter key and filter id.
*/
function faceted_search_get_filter_selection($all_filter_settings) {
$selection = array();
foreach ($all_filter_settings as $filter_key_settings) {
foreach ($filter_key_settings as $settings) {
$selection[$settings['filter_key']][$settings['filter_id']] = TRUE;
}
}
return $selection;
}
/**
* Build a search text from the specified array of filters.
*
* This can be seen as the opposite of class faceted_search's constructor, where
* a search text is parsed to build filters.
*/
function faceted_search_build_text($filters) {
$texts_per_key = array();
foreach ($filters as $filter) {
$text = $filter->get_text();
if ($text != '') {
$texts_per_key[$filter->get_key()][] = $text;
}
}
// Build the combined search text
$text = '';
foreach ($texts_per_key as $key => $texts) {
if ($text) {
$text .= ' ';
}
if ($key == 'node') {
// This is a special case where the filter's key does not appear in text.
$text .= implode(' ', $texts);
}
else {
// TODO: It is really modules that should build this text since they are
// responsible for parsing it. Or maybe it should be both built and parsed
// for them.
$text .= $key .':'. implode(',', $texts);
}
}
return trim($text);
}
function faceted_search_quoted_query_extract($keys, $option) {
// Based on search_query_extract(), but matching a quoted value. Double-quotes
// are allowed into the value when escaped.
$escape_char = variable_get('faceted_search_escape_char', '\\');
if ($escape_char == '\\') {
$escape_char .= '\\'; // Special case for regex.
}
if (preg_match('/(^| )'. $option .':"(('. $escape_char .'.|[^"])*?)"( |$)/i', $keys, $matches)) {
return faceted_search_quoted_query_unescape($matches[2]);
}
}
function faceted_search_quoted_query_insert($keys, $option, $value = '') {
// Based on search_query_insert(), but matching a quoted value. Double-quotes
// are allowed into the value when escaped.
$escape_char = variable_get('faceted_search_escape_char', '\\');
if ($escape_char == '\\') {
$escape_char .= '\\'; // Special case for regex.
}
if (!is_null(search_query_extract($keys, $option))) {
$keys = trim(preg_replace('/(^| )'. $option .':"(('. $escape_char .'.|[^"])*?)"( |$)/i', ' ', $keys));
}
if ($value != '') {
$keys .= ' '. $option .':'. $value;
}
return $keys;
}
function faceted_search_quoted_query_escape($text) {
$escape_char = variable_get('faceted_search_escape_char', '\\');
return strtr($text, array('"' => $escape_char .'"', $escape_char => $escape_char . $escape_char));
}
function faceted_search_quoted_query_unescape($text) {
$escape_char = variable_get('faceted_search_escape_char', '\\');
return strtr($text, array($escape_char .'"' => '"', $escape_char . $escape_char => $escape_char));
}
/**
* Parse text for keyword search.
*
* @return Array with positive and negative keywords.
*/
function faceted_search_parse_keywords($text) {
// Taken from search_parse_query() (search.module 1.207) - BEGIN
$keys = array('positive' => array(), 'negative' => array());
// Tokenize query string
$matches = array();
preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' '. $text, $matches, PREG_SET_ORDER);
// Classify tokens
$or = FALSE;
foreach ($matches as $match) {
$phrase = FALSE;
// Strip off phrase quotes
if ($match[2]{0} == '"') {
$match[2] = substr($match[2], 1, -1);
$phrase = TRUE;
}
// Simplify keyword according to indexing rules and external preprocessors
$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] == '-') {
$keys['negative'] = array_merge($keys['negative'], $words);
}
// OR operator: instead of a single keyword, we store an array of all
// OR'd keywords.
elseif ($match[2] == 'OR' && count($keys['positive'])) {
$last = array_pop($keys['positive']);
// Starting a new OR?
if (!is_array($last)) {
$last = array($last);
}
$keys['positive'][] = $last;
$or = TRUE;
continue;
}
// Plain keyword
else {
if ($or) {
// Add to last element (which is an array)
$keys['positive'][count($keys['positive']) - 1] = array_merge($keys['positive'][count($keys['positive']) - 1], $words);
}
else {
$keys['positive'] = array_merge($keys['positive'], $words);
}
}
$or = FALSE;
}
// Taken from search_parse_query() - END
return $keys;
}
/**
* Assign settings to filters and sort them.
*/
function faceted_search_prepare_filters(&$filters, $settings) {
if (count($filters)) {
// Assign settings to each filter.
foreach ($filters as $index => $filter) {
if (isset($settings[$filter->get_key()][$filter->get_id()])) {
$filters[$index]->set($settings[$filter->get_key()][$filter->get_id()]);
}
}
// Sort filters.
uasort($filters, '_faceted_search_compare_filters');
}
}
/**
* Implementation of hook_theme().
*/
function faceted_search_theme() {
return array(
'faceted_search_facets_settings' => array(
'arguments' => array('form' => NULL),
'file' => 'faceted_search.admin.inc',
),
'faceted_search_keyword_filters_settings' => array(
'arguments' => array('form' => NULL),
'file' => 'faceted_search.admin.inc',
),
'faceted_search_keyword_and_label' => array(
'arguments' => array('keyword' => NULL),
),
'faceted_search_keyword_phrase_label' => array(
'arguments' => array('phrase' => NULL),
),
'faceted_search_keyword_or_label' => array(
'arguments' => array('keywords' => NULL),
),
'faceted_search_keyword_not_label' => array(
'arguments' => array('keyword' => NULL),
),
);
}
function theme_faceted_search_keyword_and_label($keyword) {
return check_plain($keyword);
}
function theme_faceted_search_keyword_phrase_label($phrase) {
return check_plain($phrase);
}
function theme_faceted_search_keyword_or_label($keywords) {
foreach ($keywords as $index => $keyword) {
$keywords[$index] = check_plain($keyword);
}
return implode(' <em>OR</em> ', $keywords);
}
function theme_faceted_search_keyword_not_label($keyword) {
return '-'. check_plain($keyword);
}
/**
* Utility function to sort filters.
*/
function _faceted_search_compare_filters($a, $b) {
if ($a->get_weight() == $b->get_weight()) {
if ($a->get_key() == $b->get_key() && $a->get_id() == $b->get_id() && $a->is_active() && $b->is_active()) {
// Same filter, then sort by active category.
$a_cat = $a->get_active_category();
$b_cat = $b->get_active_category();
if ($a_cat->get_weight() == $b_cat->get_weight()) {
return strcmp($a_cat->get_label(), $b_cat->get_label());
}
return ($a_cat->get_weight() < $b_cat->get_weight()) ? -1 : 1;
}
return strcmp($a->get_label(), $b->get_label());
}
return ($a->get_weight() < $b->get_weight()) ? -1 : 1;
}
/**
* Function used by uasort in drupal_render() to sort structured arrays
* by weight.
*/
function _faceted_search_element_sort($a, $b) {
$a_weight = (is_array($a) && isset($a['#weight'])) ? $a['#weight'] : 0;
$b_weight = (is_array($b) && isset($b['#weight'])) ? $b['#weight'] : 0;
if ($a_weight == $b_weight) {
return 0;
}
return ($a_weight < $b_weight) ? -1 : 1;
}