CWIS Developer Documentation
QuickSearchHelper.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: QuickSearchHelper.php
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2002-2015 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9 
15 {
16 
26  public static function SearchField(
27  MetadataField $Field,
28  $SearchString,
29  array $IdExclusions=array(),
30  array $ValueExclusions=array())
31  {
32  $MaxResults = $Field->NumAjaxResults();
33 
34  switch ($Field->Type())
35  {
37  return self::SearchForUsers(
38  $SearchString, $MaxResults, $IdExclusions, $ValueExclusions);
39 
41  if (count($ValueExclusions))
42  {
43  throw new Exception(
44  "Cannot exclude resource by value. "
45  ."Did you want IdExclusions instead?");
46  }
47 
48  return self::SearchForResources(
49  $Field, $SearchString, $MaxResults, $IdExclusions);
50 
51  default:
52  return self::SearchForValues(
53  $Field, $SearchString, $MaxResults, $IdExclusions, $ValueExclusions);
54  }
55  }
56 
63  public static function HighlightSearchString($SearchTerms, $LabelForFormatting)
64  {
65  if(!is_array($SearchTerms))
66  {
67  $SearchTerms = array($SearchTerms);
68  }
69 
70  foreach ($SearchTerms as $SearchString)
71  {
72  $SearchString = trim($SearchString);
73  $ExplodedSearch = preg_split('/\s+/', $SearchString);
74  $Patterns = array();
75  $Index = 0;
76  $InQuote = FALSE;
77 
78  #Iterate through each term in the search string
79  foreach ($ExplodedSearch as $Term)
80  {
81  #Handle quoted terms differently
82  #if the first character is a quote
83  if ($Term[0] == '"')
84  {
85  $InQuote = TRUE;
86  }
87 
88  if (substr($Term, -1) == '"')
89  {
90  #last character is a quote means that we've found the end of the term.
91  $InQuote = FALSE;
92  }
93 
94  #remove all of the quotes if we're matched
95  $Term = str_replace('"', "", $Term);
96 
97  #Add the term to the list of patterns we'll be highlighting in the result
98  # string at the current index (quoted terms will be appended to the index,
99  # unquoted terms are added at a new index).
100  $Patterns[$Index] = (isset($Patterns[$Index]) ?
101  $Patterns[$Index]." ":"").$Term;
102 
103  if (!$InQuote)
104  {
105  # if we are not in a quoted term, the next term should go at
106  # a new index in the pattern array.
107  $Index++;
108  }
109  }
110 
111  # iterate over our terms, escaping them and including bolding
112  # for segments two more ore characters longer
113  $PregPatterns = array();
114  foreach ($Patterns as $Term)
115  {
116  if (strlen($Term)>=2)
117  {
118  $PregPatterns = "/".preg_quote($Term, "/")."/i";
119  }
120  }
121 
122  # do the highlighting
123  $LabelForFormatting = preg_replace(
124  $PregPatterns, "<b>$0</b>", $LabelForFormatting);
125 
126  }
127 
128  return $LabelForFormatting;
129  }
130 
141  public static function PrintQuickSearchField(
142  $FieldId, $CurrentValue, $CurrentDisplayValue,
143  $CloneAfter=FALSE, $FormFieldName=NULL)
144  {
145  $GLOBALS["AF"]->RequireUIFile('jquery-ui.css', ApplicationFramework::ORDER_FIRST);
146  $GLOBALS["AF"]->RequireUIFile("jquery-ui.js");
147  $GLOBALS["AF"]->RequireUIFile("CW-QuickSearch.js");
148 
149  if (is_numeric($FieldId))
150  {
151  $Field = new MetadataField($FieldId);
152 
153  if ($FormFieldName === NULL)
154  {
155  $FormFieldName = "F_".$Field->DBFieldName();
156  }
157  $SafeFieldId = intval($FieldId);
158  }
159  elseif ($FieldId == "UserSearch")
160  {
161  if ($FormFieldName === NULL)
162  {
163  throw new Exception(
164  "FormFieldName required for User Quick Search elements.");
165  }
166  $SafeFieldId = $FieldId;
167  }
168  else
169  {
170  throw new Exception("Invalid FieldId");
171  }
172 
173 
174  $SafeCurrentValue = defaulthtmlentities($CurrentValue);
175  $SafeCurrentDisplayValue = defaulthtmlentities($CurrentDisplayValue);
176  // @codingStandardsIgnoreStart
177  ?>
178  <div class="cw-quicksearch cw-quicksearch-fieldid-<?= $SafeFieldId ?>"
179  data-fieldid="<?= $SafeFieldId ?>">
180  <textarea class="cw-quicksearch-display cw-resourceeditor-metadatafield
181  <?= $FormFieldName; ?> cw-autoresize"><?= $SafeCurrentDisplayValue ?></textarea>
182  <input name="<?= $FormFieldName ?>[]"
183  class="cw-quicksearch-value" type="hidden" value="<?= $SafeCurrentValue ?>" />
184  <div style="display: none;" class='cw-quicksearch-menu'>
185  <div class='cw-quicksearch-message ui-front'></div>
186  </div>
187  </div>
188  <?PHP if ($CloneAfter) {?>
189  <div class="cw-quicksearch-template cw-quicksearch cw-quicksearch-fieldid-<?= $SafeFieldId; ?>"
190  style="display: none;" data-fieldid="<?= $SafeFieldId; ?>">
191  <textarea class="cw-quicksearch-display cw-resourceeditor-metadatafield
192  <?= $FormFieldName; ?> cw-autoresize"></textarea>
193  <input name="<?= $FormFieldName; ?>[]"
194  class="cw-quicksearch-value" type="hidden" value="<?= $SafeCurrentValue ?>" />
195  <div style="display: none;" class='cw-quicksearch-menu'>
196  <div class='cw-quicksearch-message ui-front'></div>
197  </div>
198  </div>
199  <?PHP
200  }
201  // @codingStandardsIgnoreEnd
202  }
203 
213  public static function SearchForUsers(
214  $SearchString,
215  $MaxResults = 15,
216  array $IdExclusions=array(),
217  array $ValueExclusions=array())
218  {
219  # the factory used for searching
220  $UserFactory = new CWUserFactory();
221 
222  # get the minimum word length for fuzzy query matching
223  $MysqlSysVars = new MysqlSystemVariables($GLOBALS["DB"]);
224  $MinLen = intval($MysqlSysVars->Get("ft_min_word_len"));
225 
226  # initialize the result variables
227  $SearchResults = array();
228  $ResultsNeeded = $MaxResults;
229 
230  # if the search string is less than the minimum length, do exact query
231  # matching first
232  if (strlen($SearchString) < $MinLen)
233  {
234  $SearchResults = $UserFactory->FindUserNames(
235  $SearchString,
236  "UserName", "UserName", 0, # defaults
237  PHP_INT_MAX,
238  $IdExclusions,
239  $ValueExclusions);
240 
241  # decrement the max results by how many were found
242  $ResultsNeeded -= count($SearchResults);
243  }
244 
245  # if there are still some results to fetch, perform fuzzy matching
246  if ($ResultsNeeded > 0)
247  {
248  $FuzzyResults = $UserFactory->GetMatchingUsers(
249  $SearchString,
250  "UserName");
251 
252  # filter results based on Id and Value exclusions
253  foreach ($FuzzyResults as $UserId => $Result)
254  {
255  if (!in_array($UserId, $IdExclusions) &&
256  !in_array($Result["UserName"], $ValueExclusions))
257  {
258  $SearchResults[$UserId] = $Result["UserName"];
259  }
260  }
261  }
262 
263  # slice out just the results we want
264  $TotalResults = count($SearchResults);
265  $SearchResults = array_slice($SearchResults, 0, $MaxResults, TRUE);
266 
267  $NumResults = count($SearchResults);
268  $NumAdditionalResults = $TotalResults - $NumResults;
269 
270  return array($NumResults, $NumAdditionalResults, $SearchResults);
271  }
272 
278  private static function PrepareSearchString($SearchString)
279  {
280  # remove "--", which causes searches to fail and is often in classifications
281  # Also remove unwanted punctuation
282  $SearchString = str_replace(
283  array("--",",",".", ":"), " ", $SearchString);
284 
285  # split the search string into words
286  $Words = preg_split('/\s+/', $SearchString, -1, PREG_SPLIT_NO_EMPTY);
287 
288  # the variable that holds the prepared search string
289  $PreparedSearchString = "";
290  $InQuotedString = FALSE;
291 
292  # iterate over all the words
293  foreach ($Words as $Word)
294  {
295  # include quoted strings directly
296  $InQuotedString |= preg_match('/^[+-]?"/', $Word);
297  if ($InQuotedString)
298  {
299  $PreparedSearchString .= $Word." ";
300  $InQuotedString &= (substr($Word, -1) != '"');
301  }
302  # append a * to every word outside a quoted string
303  elseif (strlen($Word)>1)
304  {
305  $PreparedSearchString .= $Word."* ";
306  }
307  }
308 
309  # clean up trailing whitespace, ensure quotes are closed
310  $PreparedSearchString = trim($PreparedSearchString);
311  if ($InQuotedString)
312  {
313  $PreparedSearchString .= '"';
314  }
315 
316  return $PreparedSearchString;
317  }
318 
343  private static function SortSearchResults($Results, $SearchString, $MaxResults)
344  {
345  $Matches = array(
346  "Exact" => array(),
347  "End" => array(),
348  "BegSp" => array(),
349  "Beg" => array(),
350  "MidSp" => array(),
351  "Mid" => array(),
352  "Other" => array() );
353 
354  # escape regex characters
355  $SafeStr = preg_quote( trim( preg_replace('/\s+/', " ",
356  str_replace( array("--",",",".", ":"), " ",
357  $SearchString) )), '/');
358 
359  # iterate over search results, sorting them into bins
360  foreach ($Results as $Key => $Val)
361  {
362  # apply the same normalization to our value as we did our search string
363  $TestVal = preg_quote( trim( preg_replace('/\s+/', " ",
364  str_replace( array("--",",",".", ":"), " ",
365  $Val) )), '/');
366 
367  if (preg_match('/^'.$SafeStr.'$/i', $TestVal))
368  {
369  $ix = "Exact";
370  }
371  elseif (preg_match('/^'.$SafeStr.'\\W/i', $TestVal))
372  {
373  $ix = "BegSp";
374  }
375  elseif (preg_match('/^'.$SafeStr.'/i', $TestVal))
376  {
377  $ix = "Beg";
378  }
379  elseif (preg_match('/'.$SafeStr.'$/i', $TestVal))
380  {
381  $ix = "End";
382  }
383  elseif (preg_match('/'.$SafeStr.'\\W/i', $TestVal))
384  {
385  $ix = "MidSp";
386  }
387  elseif (preg_match('/'.$SafeStr.'/i', $TestVal))
388  {
389  $ix = "Mid";
390  }
391  else
392  {
393  $ix = "Other";
394  }
395 
396  $Matches[$ix][$Key] = $Val;
397  }
398 
399  # assemble the sorted results
400  $SortedResults = array();
401  foreach (array("Exact", "BegSp", "Beg", "End", "MidSp", "Mid", "Other") as $ix)
402  {
403  asort( $Matches[$ix] );
404  $SortedResults += $Matches[$ix];
405  }
406 
407  # trim down the list to the requested number
408  $SortedResults = array_slice($SortedResults, 0, $MaxResults, TRUE);
409 
410  return $SortedResults;
411  }
412 
424  private static function SearchForResources(
425  $DstField,
426  $SearchString,
427  $MaxResults,
428  array $IdExclusions=array() )
429  {
430  # construct search groups based on the keyword
431  $SearchParams = new SearchParameterSet();
432  $SearchParams->AddParameter($SearchString);
433 
434  $SignalResult = $GLOBALS["AF"]->SignalEvent(
435  "EVENT_FIELD_SEARCH_FILTER",
436  array(
437  "Search" => $SearchParams,
438  "Field" => $DstField));
439  $SearchParams = $SignalResult["Search"];
440 
441  # perform search
442  $SearchEngine = new SPTSearchEngine();
443  $SearchResults = $SearchEngine->Search($SearchParams);
444 
445  # get the list of referenceable schemas for this field
446  $ReferenceableSchemaIds = $DstField->ReferenceableSchemaIds();
447 
448  # iterate over search results from desired schemas
449  $SearchResultsNew = array();
450  foreach ($SearchResults as $SchemaId => $SchemaResults)
451  {
452  if (in_array($SchemaId, $ReferenceableSchemaIds))
453  {
454  # filter resources the user cannot see
455  $RFactory = new ResourceFactory($SchemaId);
456  $ViewableIds = $RFactory->FilterNonViewableResources(
457  array_keys($SchemaResults), $GLOBALS["G_User"]);
458 
459  # add these results to our list of all search results
460  $SearchResultsNew += array_intersect_key(
461  $SchemaResults, array_flip($ViewableIds));
462  }
463  }
464  $SearchResults = $SearchResultsNew;
465 
466  # filter out excluded resource IDs if necessary
467  if (count($IdExclusions))
468  {
469  $SearchResults = array_diff_key(
470  $SearchResults, array_flip($IdExclusions));
471  }
472 
473  # pull out mapped titles for all resources
474  $GLOBALS["AF"]->LoadFunction("GetResourceFieldValue");
475  $ResourceData = array();
476  foreach ($SearchResults as $ResourceId => $Score)
477  {
478  $Resource = new Resource($ResourceId);
479  $ResourceData[$ResourceId] = GetResourceFieldValue(
480  $Resource,
481  $Resource->Schema()->GetFieldByMappedName("Title") );
482  }
483 
484  # determine how many results we had in total
485  $TotalResults = count($ResourceData);
486 
487  # sort resources by title and subset if necessary
488  $ResourceData = self::SortSearchResults(
489  $ResourceData,
490  $SearchString,
491  $MaxResults);
492 
493  # compute the number of available and additional results
494  $NumSearchResults = count($ResourceData);
495  $NumAdditionalSearchResults = $TotalResults - count($ResourceData);
496 
497  return array($NumSearchResults, $NumAdditionalSearchResults, $ResourceData);
498  }
499 
511  private static function SearchForValues(
512  MetadataField $Field,
513  $SearchString,
514  $MaxResults,
515  array $IdExclusions,
516  array $ValueExclusions)
517  {
518  $Factory = $Field->GetFactory();
519 
520  # get the minimum word length for fuzzy query matching
521  $MysqlSysVars = new MysqlSystemVariables($GLOBALS["DB"]);
522  $MinLen = intval($MysqlSysVars->Get("ft_min_word_len"));
523 
524  # initialize the result variables
525  $Results = array();
526  $Total = 0;
527 
528  $SignalResult = $GLOBALS["AF"]->SignalEvent(
529  "EVENT_FIELD_SEARCH_FILTER",
530  array(
531  "Search" => $SearchString,
532  "Field" => $Field));
533  $SearchString = $SignalResult["Search"];
534 
535  # if the search string is less than the minimum length, do exact query
536  # matching first
537  if (strlen($SearchString) < $MinLen)
538  {
539  # search for results and get the total
540  $Results += $Factory->SearchForItemNames(
541  $SearchString,
542  $MaxResults,
543  FALSE, TRUE, 0, # defaults
544  $IdExclusions,
545  $ValueExclusions);
546  $Total += $Factory->GetCountForItemNames(
547  $SearchString,
548  FALSE, TRUE, # defaults,
549  $IdExclusions,
550  $ValueExclusions);
551 
552  # decrement the max results by how many were returned when doing exact
553  # matching
554  $MaxResults -= count($Results);
555  }
556 
557  # if more results should be fetched
558  if ($MaxResults > 0)
559  {
560  $PreparedSearchString = self::PrepareSearchString($SearchString);
561 
562  if (strlen($SearchString) >= $MinLen)
563  {
564  $Results += $Factory->FindMatchingRecentlyUsedValues(
565  $PreparedSearchString, 5, $IdExclusions, $ValueExclusions);
566 
567  if (count($Results))
568  {
569  $Results += array(-1 => "<hr>");
570  }
571  }
572 
573  # search for results and get the total
574  $Results += self::SortSearchResults(
575  $Factory->SearchForItemNames(
576  $PreparedSearchString,
577  2000,
578  FALSE, TRUE, 0, # defaults
579  $IdExclusions,
580  $ValueExclusions),
581  $SearchString,
582  $MaxResults);
583  $Total += $Factory->GetCountForItemNames(
584  $PreparedSearchString,
585  FALSE, TRUE, # defaults,
586  $IdExclusions,
587  $ValueExclusions);
588  }
589 
590  # get additional totals
591  $NumSearchResults = count($Results);
592  $NumAdditionalSearchResults = $Total - $NumSearchResults;
593 
594  return array($NumSearchResults, $NumAdditionalSearchResults, $Results);
595  }
596 }
Set of parameters used to perform a search.
GetFactory()
Retrieve item factory object for this field.
static HighlightSearchString($SearchTerms, $LabelForFormatting)
Highlight all instances of the search string in the result label.
static PrintQuickSearchField($FieldId, $CurrentValue, $CurrentDisplayValue, $CloneAfter=FALSE, $FormFieldName=NULL)
Print the blank text field quick search field for the QuickSearch JS object.
static SearchField(MetadataField $Field, $SearchString, array $IdExclusions=array(), array $ValueExclusions=array())
Search a field for values matching a specified search string.
Class that allows permits easier access to MySQL system variables.
CWIS-specific user factory class.
const ORDER_FIRST
Handle item first (i.e.
Object representing a locally-defined type of metadata field.
NumAjaxResults($NewValue=DB_NOVALUE)
Get/set the maximum number of results to display in an AJAX dropdown.
Represents a "resource" in CWIS.
Definition: Resource.php:13
static SearchForUsers($SearchString, $MaxResults=15, array $IdExclusions=array(), array $ValueExclusions=array())
Perform a search for users.
Type($NewValue=DB_NOVALUE)
Get/set type of metadata field (enumerated value).
Convenience class for QuickSearch responses, making it easy to share functions common to different ty...
Factory for Resource objects.