]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/SearchForm/SugarSpot.php
Merge pull request #100 from collinlee/master
[Github/sugarcrm.git] / include / SearchForm / SugarSpot.php
1 <?php
2 //if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4  * SugarCRM Community Edition is a customer relationship management program developed by
5  * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
6  * 
7  * This program is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU Affero General Public License version 3 as published by the
9  * Free Software Foundation with the addition of the following permission added
10  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11  * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13  * 
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
17  * details.
18  * 
19  * You should have received a copy of the GNU Affero General Public License along with
20  * this program; if not, see http://www.gnu.org/licenses or write to the Free
21  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22  * 02110-1301 USA.
23  * 
24  * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25  * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
26  * 
27  * The interactive user interfaces in modified source and object code versions
28  * of this program must display Appropriate Legal Notices, as required under
29  * Section 5 of the GNU Affero General Public License version 3.
30  * 
31  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32  * these Appropriate Legal Notices must retain the display of the "Powered by
33  * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34  * technical reasons, the Appropriate Legal Notices must display the words
35  * "Powered by SugarCRM".
36  ********************************************************************************/
37
38
39 /**
40  * Global search
41  * @api
42  */
43 class SugarSpot
44 {
45         /**
46      * searchAndDisplay
47      *
48          * Performs the search and returns the HTML widget containing the results
49          *
50          * @param  $query string what we are searching for
51          * @param  $modules array modules we are searching in
52          * @param  $offset int search result offset
53          * @return string HTML code containing results
54          */
55         public function searchAndDisplay($query, $modules, $offset=-1)
56         {
57         global $current_user;
58                 $query_encoded = urlencode($query);
59             $results = $this->_performSearch($query, $modules, $offset);
60         $displayResults = array();
61         $displayMoreForModule = array();
62
63                 foreach($results as $m=>$data)
64         {
65                         if(empty($data['data']))
66             {
67                                 continue;
68                         }
69
70             $total = count($data['data']);
71                         $countRemaining = $data['pageData']['offsets']['total'] - $total;
72
73             if(isset($results[$m]['readAccess']) && !$results[$m]['readAccess'])
74             {
75                $displayTotal = $countRemaining > 0 ? ($total + $countRemaining) : $total;
76                $displayResults[$m]['link'] = array('total'=>$displayTotal, 'count_remaining'=>$countRemaining, 'query_encoded'=>$query_encoded);
77                continue;
78             }
79
80                         if($offset > 0)
81             {
82                 $countRemaining -= $offset;
83             }
84
85                         if($countRemaining > 0)
86             {
87                 $displayMoreForModule[$m] = array('query'=>$query,
88                                                   'offset'=>$data['pageData']['offsets']['next']++,
89                                                   'countRemaining'=>$countRemaining);
90                         }
91
92             foreach($data['data'] as $row)
93             {
94                                 $name = '';
95
96                 //Determine a name to use
97                                 if(!empty($row['NAME']))
98                 {
99                                         $name = $row['NAME'];
100                                 } else if(!empty($row['DOCUMENT_NAME'])) {
101                                     $name = $row['DOCUMENT_NAME'];
102                                 } else {
103                     $foundName = '';
104                                         foreach($row as $k=>$v){
105                                                 if(strpos($k, 'NAME') !== false)
106                         {
107                             if(!empty($row[$k]))
108                             {
109                                                             $name = $v;
110                                                             break;
111                             } else if(empty($foundName)) {
112                                 $foundName = $v;
113                             }
114                                                 }
115                                         }
116
117                                         if(empty($name))
118                                         {
119                                            $name = $foundName;
120                                         }
121                                 }
122
123                                 $displayResults[$m][$row['ID']] = $name;
124
125                     }
126
127
128
129         }
130         $ss = new Sugar_Smarty();
131         $ss->assign('displayResults', $displayResults);
132         $ss->assign('displayMoreForModule', $displayMoreForModule);
133         $ss->assign('appStrings', $GLOBALS['app_strings']);
134         $ss->assign('appListStrings', $GLOBALS['app_list_strings']);
135         $ss->assign('queryEncoded', $query_encoded);
136         $template = 'include/SearchForm/tpls/SugarSpot.tpl';
137         if(file_exists('custom/include/SearchForm/tpls/SugarSpot.tpl'))
138         {
139             $template = 'custom/include/SearchForm/tpls/SugarSpot.tpl';
140         }
141         return $ss->fetch($template);
142         }
143
144         /**
145          * Returns the array containing the $searchFields for a module.  This function
146          * first checks the default installation directories for the SearchFields.php file and then
147          * loads any custom definition (if found)
148          *
149          * @param  $moduleName String name of module to retrieve SearchFields entries for
150          * @return array of SearchFields
151          */
152         protected static function getSearchFields(
153             $moduleName
154             )
155         {
156                 $searchFields = array();
157
158                 if(file_exists("modules/{$moduleName}/metadata/SearchFields.php"))
159                 {
160                     require("modules/{$moduleName}/metadata/SearchFields.php");
161                 }
162
163                 if(file_exists("custom/modules/{$moduleName}/metadata/SearchFields.php"))
164                 {
165                     require("custom/modules/{$moduleName}/metadata/SearchFields.php");
166                 }
167
168                 return $searchFields;
169         }
170
171
172         /**
173          * Get count from query
174          * @param SugarBean $seed
175          * @param string $main_query
176          */
177         protected function _getCount($seed, $main_query)
178         {
179 //        $count_query = $seed->create_list_count_query($main_query);
180                 $result = $seed->db->query("SELECT COUNT(*) as c FROM ($main_query) main");
181                 $row = $seed->db->fetchByAssoc($result);
182                 return isset($row['c'])?$row['c']:0;
183         }
184
185         /**
186      * _performSearch
187      *
188          * Performs the search from the global search field.
189          *
190          * @param  $query   string what we are searching for
191          * @param  $modules array  modules we are searching in
192          * @param  $offset  int    search result offset
193          * @return array
194          */
195         protected function _performSearch(
196             $query,
197             $modules,
198             $offset = -1
199             )
200         {
201         //Return an empty array if no query string is given
202             if(empty($query))
203         {
204             return array();
205         }
206
207                 $primary_module='';
208                 $results = array();
209                 require_once 'include/SearchForm/SearchForm2.php' ;
210                 $where = '';
211         $searchEmail = preg_match('/^([^%]|%)*@([^%]|%)*$/', $query);
212
213         // bug49650 - strip out asterisks from query in case
214         // user thinks asterisk is a wildcard value
215         $query = str_replace( '*' , '' , $query );
216         
217         $limit = !empty($GLOBALS['sugar_config']['max_spotresults_initial']) ? $GLOBALS['sugar_config']['max_spotresults_initial'] : 5;
218                 if($offset !== -1){
219                         $limit = !empty($GLOBALS['sugar_config']['max_spotresults_more']) ? $GLOBALS['sugar_config']['max_spotresults_more'] : 20;
220                 }
221         $totalCounted = empty($GLOBALS['sugar_config']['disable_count_query']);
222
223
224         global $current_user;
225
226             foreach($modules as $moduleName)
227         {
228                     if (empty($primary_module))
229                     {
230                         $primary_module=$moduleName;
231                     }
232
233                         $searchFields = SugarSpot::getSearchFields($moduleName);
234
235             //Continue on to the next module if no search fields found for module
236                         if (empty($searchFields[$moduleName]))
237                         {
238                                 continue;
239                         }
240
241                         $return_fields = array();
242                         $seed = SugarModule::get($moduleName)->loadBean();
243
244             //Continue on to next module if we don't have ListView ACLAccess for module
245             if(!$seed->ACLAccess('ListView')) {
246                continue;
247             }
248
249             $class = $seed->object_name;
250
251                         foreach($searchFields[$moduleName] as $k=>$v)
252             {
253                                 $keep = false;
254                                 $searchFields[$moduleName][$k]['value'] = $query;
255
256                 //If force_unifiedsearch flag is true, we are essentially saying this field must be searched on (e.g. search_name in SearchFields.php file)
257                 if(!empty($searchFields[$moduleName][$k]['force_unifiedsearch']))
258                 {
259                     continue;
260                 }
261
262                                 if(!empty($GLOBALS['dictionary'][$class]['unified_search'])){
263
264                                         if(empty($GLOBALS['dictionary'][$class]['fields'][$k]['unified_search'])){
265
266                                                 if(isset($searchFields[$moduleName][$k]['db_field'])){
267                                                         foreach($searchFields[$moduleName][$k]['db_field'] as $field)
268                             {
269                                                                 if(!empty($GLOBALS['dictionary'][$class]['fields'][$field]['unified_search']))
270                                 {
271                                     if(isset($GLOBALS['dictionary'][$class]['fields'][$field]['type']))
272                                     {
273                                         if(!$this->filterSearchType($GLOBALS['dictionary'][$class]['fields'][$field]['type'], $query))
274                                         {
275                                             unset($searchFields[$moduleName][$k]);
276                                             continue;
277                                         }
278                                     }
279
280                                     $keep = true;
281                                                                 }
282                                                         } //foreach
283                                                 }
284                         # Bug 42961 Spot search for custom fields
285                         if (!$keep && (isset($v['force_unifiedsearch']) == false || $v['force_unifiedsearch'] != true))
286                         {
287                                                         if(strpos($k,'email') === false || !$searchEmail) {
288                                                                 unset($searchFields[$moduleName][$k]);
289                                                         }
290                                                 }
291                                         }else{
292                                             if($GLOBALS['dictionary'][$class]['fields'][$k]['type'] == 'int' && !is_numeric($query)) {
293                                                 unset($searchFields[$moduleName][$k]);
294                                             }
295                                         }
296                                 }else if(empty($GLOBALS['dictionary'][$class]['fields'][$k]) ){
297                                         //If module did not have unified_search defined, then check the exception for an email search before we unset
298                                         if(strpos($k,'email') === false || !$searchEmail)
299                                         {
300                                            unset($searchFields[$moduleName][$k]);
301                                         }
302                                 }else if(!$this->filterSearchType($GLOBALS['dictionary'][$class]['fields'][$k]['type'], $query)){
303                     unset($searchFields[$moduleName][$k]);
304                                 }
305                         } //foreach
306
307             //If no search field criteria matched then continue to next module
308                         if (empty($searchFields[$moduleName]))
309             {
310                 continue;
311             }
312
313             //Variable used to store the "name" field displayed in results
314             $name_field = null;
315
316                         if(isset($seed->field_defs['name']))
317             {
318                             $return_fields['name'] = $seed->field_defs['name'];
319                 $name_field = 'name';
320                         }
321
322
323                         foreach($seed->field_defs as $k => $v)
324             {
325                             if(isset($seed->field_defs[$k]['type']) && ($seed->field_defs[$k]['type'] == 'name') && !isset($return_fields[$k]))
326                 {
327                                     $return_fields[$k] = $seed->field_defs[$k];
328                                 }
329                         }
330
331
332                         if(!isset($return_fields['name']))
333             {
334                             // if we couldn't find any name fields, try search fields that have name in it
335                             foreach($searchFields[$moduleName] as $k => $v)
336                 {
337                                 if(strpos($k, 'name') != -1 && isset($seed->field_defs[$k]) && !isset($seed->field_defs[$k]['source']))
338                     {
339                                         $return_fields[$k] = $seed->field_defs[$k];
340                         $name_field = $k;
341                                         break;
342                                     }
343                             }
344                         }
345
346
347                         if(!isset($return_fields['name']))
348             {
349                             // last resort - any fields that have 'name' in their name
350                             foreach($seed->field_defs as $k => $v)
351                 {
352                     if(strpos($k, 'name') != -1 && isset($seed->field_defs[$k]) && !isset($seed->field_defs[$k]['source']))
353                     {
354                                         $return_fields[$k] = $seed->field_defs[$k];
355                                         $name_field = $k;
356                         break;
357                                     }
358                             }
359                         }
360
361
362                         if(empty($name_field)) {
363                             // FAIL: couldn't find a name field to display a result label
364                             $GLOBALS['log']->error("Unable to find name field for module $moduleName");
365                             continue;
366                         }
367
368                         if(isset($return_fields['name']['fields']))
369             {
370                             // some names are composite name fields (e.g. last_name, first_name), add these to return list
371                             foreach($return_fields['name']['fields'] as $field)
372                 {
373                                 $return_fields[$field] = $seed->field_defs[$field];
374                             }
375                         } 
376
377                         $searchForm = new SearchForm ( $seed, $moduleName ) ;
378                         $searchForm->setup (array ( $moduleName => array() ) , $searchFields , '' , 'saved_views' /* hack to avoid setup doing further unwanted processing */ ) ;
379                         $where_clauses = $searchForm->generateSearchWhere() ;
380
381                         if(empty($where_clauses))
382             {
383                             continue;
384                         }
385                         if(count($where_clauses) > 1) {
386                             $query_parts =  array();
387
388                             $ret_array_start = $seed->create_new_list_query('', '', $return_fields, array(), 0, '', true, $seed, true);
389                 $search_keys = array_keys($searchFields[$moduleName]);
390
391                 foreach($where_clauses as $n => $clause) {
392                                 $allfields = $return_fields;
393                                 $skey = $search_keys[$n];
394                                 if(isset($seed->field_defs[$skey])) {
395                         // Joins for foreign fields aren't produced unless the field is in result, hence the merge
396                                     $allfields[$skey] = $seed->field_defs[$skey];
397                                 }
398                     $ret_array = $seed->create_new_list_query('', $clause, $allfields, array(), 0, '', true, $seed, true);
399                     $query_parts[] = $ret_array_start['select'] . $ret_array['from'] . $ret_array['where'] . $ret_array['order_by'];
400                 }
401                 $main_query = "(".join(") UNION (", $query_parts).")";
402                         } else {
403                 foreach($searchFields[$moduleName] as $k=>$v){
404                     if(isset($seed->field_defs[$k])) {
405                                     $return_fields[$k] = $seed->field_defs[$k];
406                     }
407                             }
408                             $ret_array = $seed->create_new_list_query('', $where_clauses[0], $return_fields, array(), 0, '', true, $seed, true);
409                         $main_query = $ret_array['select'] . $ret_array['from'] . $ret_array['where'] . $ret_array['order_by'];
410                         }
411
412                         $totalCount = null;
413                     if($limit < -1) {
414                             $result = $seed->db->query($main_query);
415                     } else {
416                             if($limit == -1) {
417                                     $limit = $GLOBALS['sugar_config']['list_max_entries_per_page'];
418                 }
419
420                 if($offset == 'end') {
421                             $totalCount = $this->_getCount($seed, $main_query);
422                             if($totalCount) {
423                             $offset = (floor(($totalCount -1) / $limit)) * $limit;
424                             } else {
425                                 $offset = 0;
426                             }
427                 }
428                 $result = $seed->db->limitQuery($main_query, $offset, $limit + 1);
429                     }
430
431             $data = array();
432             $count = 0;
433             while($count < $limit && ($row = $seed->db->fetchByAssoc($result))) {
434                         $temp = clone $seed;
435                             $temp->setupCustomFields($temp->module_dir);
436                                 $temp->loadFromRow($row);
437                                 $data[] = $temp->get_list_view_data($return_fields);
438                                 $count++;
439                     }
440
441                 $nextOffset = -1;
442                 $prevOffset = -1;
443                 $endOffset = -1;
444
445                 if($count >= $limit) {
446                         $nextOffset = $offset + $limit;
447                 }
448
449                 if($offset > 0) {
450                         $prevOffset = $offset - $limit;
451                         if($prevOffset < 0) $prevOffset = 0;
452                 }
453
454                 if( $count >= $limit && $totalCounted){
455                     if(!isset($totalCount)) {
456                             $totalCount  = $this->_getCount($seed, $main_query);
457                     }
458                 } else {
459                     $totalCount = $count + $offset;
460                 }
461
462             $pageData['offsets'] = array( 'current'=>$offset, 'next'=>$nextOffset, 'prev'=>$prevOffset, 'end'=>$endOffset, 'total'=>$totalCount, 'totalCounted'=>$totalCounted);
463                     $pageData['bean'] = array('objectName' => $seed->object_name, 'moduleDir' => $seed->module_dir);
464
465             $readAccess = true;
466
467                     $results[$moduleName] = array("data"=>$data, "pageData"=>$pageData, "readAccess"=>$readAccess);
468
469                 } //foreach
470
471         return $results;
472         }
473
474
475         /**
476      * Function used to walk the array and find keys that map the queried string.
477      * if both the pattern and module name is found the promote the string to thet top.
478      */
479     protected function _searchKeys(
480         $item1,
481         $key,
482         $patterns
483         )
484     {
485         //make the module name singular....
486         if ($patterns[1][strlen($patterns[1])-1] == 's') {
487             $patterns[1]=substr($patterns[1],0,(strlen($patterns[1])-1));
488         }
489
490         $module_exists = stripos($key,$patterns[1]); //primary module name.
491         $pattern_exists = stripos($key,$patterns[0]); //pattern provided by the user.
492         if ($module_exists !== false and $pattern_exists !== false)  {
493             $GLOBALS['matching_keys']= array_merge(array(array('NAME'=>$key, 'ID'=>$key, 'VALUE'=>$item1)),$GLOBALS['matching_keys']);
494         }
495         else {
496             if ($pattern_exists !== false) {
497                 $GLOBALS['matching_keys'][]=array('NAME'=>$key, 'ID'=>$key, 'VALUE'=>$item1);
498             }
499         }
500     }
501
502
503     /**
504      * filterSearchType
505      *
506      * This is a private function to determine if the search type field should be filtered out based on the query string value
507      * 
508      * @param String $type The string value of the field type (e.g. phone, date, datetime, int, etc.)
509      * @param String $query The search string value sent from the global search
510      * @return boolean True if the search type fits the query string value; false otherwise
511      */
512     protected function filterSearchType($type, $query)
513     {
514         switch($type)
515         {
516             case 'id':
517             case 'date':
518             case 'datetime':
519             case 'bool':
520                 return false;
521                 break;
522             case 'int':
523                 if(!is_numeric($query)) {
524                    return false;
525                 }
526                 break;
527             case 'phone':
528                 //For a phone search we require at least three digits
529                 if(!preg_match('/[0-9]{3,}/', $query))
530                 {
531                     return false;
532                 }
533             case 'decimal':
534             case 'float':
535                 if(!preg_match('/[0-9]/', $query))
536                 {
537                    return false;
538                 }
539                 break;
540         }
541         return true;
542     }
543
544 }