1: <?php
2: /**
3: * This file contains various classes for content search.
4: * API to index a CONTENIDO article
5: * API to search in the index structure
6: * API to display the searchresults
7: *
8: * @package Core
9: * @subpackage Frontend_Search
10: * @version SVN Revision $Rev:$
11: *
12: * @author Willi Man
13: * @copyright four for business AG <www.4fb.de>
14: * @license http://www.contenido.org/license/LIZENZ.txt
15: * @link http://www.4fb.de
16: * @link http://www.contenido.org
17: */
18: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
19:
20: cInclude('includes', 'functions.encoding.php');
21:
22: /**
23: * Abstract base search class.
24: * Provides general properties and functions
25: * for child implementations.
26: *
27: * @author Murat Purc <murat@purc.de>
28: *
29: * @package Core
30: * @subpackage Frontend_Search
31: */
32: abstract class cSearchBaseAbstract {
33:
34: /**
35: * CONTENIDO database object
36: *
37: * @var cDb
38: */
39: protected $oDB;
40:
41: /**
42: * CONTENIDO configuration data
43: *
44: * @var array
45: */
46: protected $cfg;
47:
48: /**
49: * Language id of a client
50: *
51: * @var int
52: */
53: protected $lang;
54:
55: /**
56: * Client id
57: *
58: * @var int
59: */
60: protected $client;
61:
62: /**
63: * Initialises some properties
64: *
65: * @param cDb $oDB Optional database instance
66: * @param bool $bDebug Optional, flag to enable debugging (no longer needed)
67: */
68: protected function __construct($oDB = null, $bDebug = false) {
69: global $cfg, $lang, $client;
70:
71: $this->cfg = $cfg;
72: $this->lang = $lang;
73: $this->client = $client;
74:
75: $this->bDebug = $bDebug;
76:
77: if ($oDB == null || !is_object($oDB)) {
78: $this->db = cRegistry::getDb();
79: } else {
80: $this->db = $oDB;
81: }
82: }
83:
84: /**
85: * Main debug function, prints dumps parameter if debugging is enabled
86: *
87: * @param string $msg Some text
88: * @param mixed $var The variable to dump
89: */
90: protected function _debug($msg, $var) {
91: $dump = $msg . ': ';
92: if (is_array($var) || is_object($var)) {
93: $dump .= print_r($var, true);
94: } else {
95: $dump .= $var;
96: }
97: cDebug::out($dump);
98: }
99:
100: }
101:
102: /**
103: * CONTENIDO API - Search Index Object
104: *
105: * This object creates an index of an article
106: *
107: * Create object with
108: * $oIndex = new SearchIndex($db); # where $db is the global CONTENIDO database
109: * object.
110: * Start indexing with
111: * $oIndex->start($idart, $aContent);
112: * where $aContent is the complete content of an article specified by its
113: * content types.
114: * It looks like
115: * Array (
116: * [CMS_HTMLHEAD] => Array (
117: * [1] => Herzlich Willkommen...
118: * [2] => ...auf Ihrer Website!
119: * )
120: * [CMS_HTML] => Array (
121: * [1] => Die Inhalte auf dieser Website ...
122: *
123: * The index for keyword 'willkommen' would look like '&12=1(CMS_HTMLHEAD-1)'
124: * which means the keyword 'willkommen' occurs 1 times in article with articleId
125: * 12 and content type CMS_HTMLHEAD[1].
126: *
127: * TODO: The basic idea of the indexing process is to take the complete content
128: * of an article and to generate normalized index terms
129: * from the content and to store a specific index structure in the relation
130: * 'con_keywords'.
131: * To take the complete content is not very flexible. It would be better to
132: * differentiate by specific content types or by any content.
133: * The &, =, () and - seperated string is not easy to parse to compute the
134: * search result set.
135: * It would be a better idea (and a lot of work) to extend the relation
136: * 'con_keywords' to store keywords by articleId (or content source identifier)
137: * and content type.
138: * The functions removeSpecialChars, setStopwords, setContentTypes and
139: * setCmsOptions should be sourced out into a new helper-class.
140: * Keep in mind that class Search and SearchResult uses an instance of object
141: * Index.
142: *
143: * @package Core
144: * @subpackage Frontend_Search
145: */
146: class cSearchIndex extends cSearchBaseAbstract {
147:
148: /**
149: * the content of the cms-types of an article
150: *
151: * @var array
152: */
153: protected $_keycode = array();
154:
155: /**
156: * the list of keywords of an article
157: *
158: * @var array
159: */
160: protected $_keywords = array();
161:
162: /**
163: * the words, which should not be indexed
164: *
165: * @var array
166: */
167: protected $_stopwords = array();
168:
169: /**
170: * the keywords of an article stored in the DB
171: *
172: * @var array
173: */
174: protected $_keywordsOld = array();
175:
176: /**
177: * the keywords to be deleted
178: *
179: * @var array
180: */
181: protected $_keywordsDel = array();
182:
183: /**
184: * 'auto' or 'self'
185: * The field 'auto' in table con_keywords is used for automatic indexing.
186: * The value is a string like "&12=2(CMS_HTMLHEAD-1,CMS_HTML-1)", which
187: * means a keyword occurs 2 times in article with $idart 12
188: * and can be found in CMS_HTMLHEAD[1] and CMS_HTML[1].
189: * The field 'self' can be used in the article properties to index the
190: * article manually.
191: *
192: * @var string
193: */
194: protected $_place;
195:
196: /**
197: * array of cms types
198: *
199: * @var array
200: */
201: protected $_cmsOptions = array();
202:
203: /**
204: * array of all available cms types
205: *
206: * htmlhead - HTML Headline
207: * html - HTML Text
208: * head - Headline (no HTML)
209: * text - Text (no HTML)
210: * img - Upload id of the element
211: * imgdescr - Image description
212: * link - Link (URL)
213: * linktarget - Linktarget (_self, _blank, _top ...)
214: * linkdescr - Linkdescription
215: * swf - Upload id of the element
216: * etc.
217: *
218: * @var array
219: */
220: protected $_cmsType = array();
221:
222: /**
223: * the suffix of all available cms types
224: *
225: * @var array
226: */
227: protected $_cmsTypeSuffix = array();
228:
229: /**
230: * Constructor, set object properties
231: *
232: * @param cDb $db CONTENIDO Database object
233: * @return void
234: */
235: public function __construct($db = null) {
236: parent::__construct($db);
237:
238: $this->setContentTypes();
239: }
240:
241: /**
242: * Start indexing the article.
243: *
244: * @param int $idart Article Id
245: * @param array $aContent The complete content of an article specified by
246: * its content types.
247: * It looks like
248: * Array (
249: * [CMS_HTMLHEAD] => Array (
250: * [1] => Herzlich Willkommen...
251: * [2] => ...auf Ihrer Website!
252: * )
253: * [CMS_HTML] => Array (
254: * [1] => Die Inhalte auf dieser Website ...
255: *
256: * @param string $place The field where to store the index information in
257: * db.
258: * @param array $cms_options One can specify explicitly cms types which
259: * should not be indexed.
260: * @param array $aStopwords Array with words which should not be indexed.
261: */
262: public function start($idart, $aContent, $place = 'auto', $cms_options = array(), $aStopwords = array()) {
263: if (!is_int((int) $idart) || $idart < 0) {
264: return null;
265: } else {
266: $this->idart = $idart;
267: }
268:
269: $this->_place = $place;
270: $this->_keycode = $aContent;
271: $this->setStopwords($aStopwords);
272: $this->setCmsOptions($cms_options);
273:
274: $this->createKeywords();
275:
276: $this->getKeywords();
277:
278: $this->saveKeywords();
279:
280: $new_keys = array_keys($this->_keywords);
281: $old_keys = array_keys($this->_keywordsOld);
282:
283: $this->_keywordsDel = array_diff($old_keys, $new_keys);
284:
285: if (count($this->_keywordsDel) > 0) {
286: $this->deleteKeywords();
287: }
288: }
289:
290: /**
291: * for each cms-type create index structure.
292: * it looks like
293: * Array (
294: * [die] => CMS_HTML-1
295: * [inhalte] => CMS_HTML-1
296: * [auf] => CMS_HTML-1 CMS_HTMLHEAD-2
297: * [dieser] => CMS_HTML-1
298: * [website] => CMS_HTML-1 CMS_HTML-1 CMS_HTMLHEAD-2
299: * )
300: */
301: public function createKeywords() {
302: $tmp_keys = array();
303:
304: // Only create keycodes, if some are available
305: if (is_array($this->_keycode)) {
306: foreach ($this->_keycode as $idtype => $data) {
307: if ($this->checkCmsType($idtype)) {
308: foreach ($data as $typeid => $code) {
309: $this->_debug('code', $code);
310:
311: // remove backslash
312: $code = stripslashes($code);
313: // replace HTML line breaks with newlines
314: $code = str_ireplace(array(
315: '<br>',
316: '<br />'
317: ), "\n", $code);
318: // remove html tags
319: $code = strip_tags($code);
320: if (strlen($code) > 0) {
321: $code = conHtmlEntityDecode($code);
322: }
323: $this->_debug('code', $code);
324:
325: // split content by any number of commas or space
326: // characters
327: $tmp_keys = preg_split('/[\s,]+/', trim($code));
328: $this->_debug('tmp_keys', $tmp_keys);
329:
330: foreach ($tmp_keys as $value) {
331: // index terms are stored with lower case
332: // $value = strtolower($value);
333:
334: $value = htmlentities($value, ENT_COMPAT, 'UTF-8');
335: $value = trim(strtolower($value));
336: $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
337:
338: if (!in_array($value, $this->_stopwords)) {
339: // eliminate stopwords
340: $value = $this->removeSpecialChars($value);
341:
342: if (strlen($value) > 1) {
343: // do not index single characters
344: $this->_keywords[$value] = $this->_keywords[$value] . $idtype . '-' . $typeid . ' ';
345: }
346: }
347: }
348: }
349: }
350:
351: unset($tmp_keys);
352: }
353: }
354:
355: $this->_debug('keywords', $this->_keywords);
356: }
357:
358: /**
359: * generate index_string from index structure and save keywords
360: * The index_string looks like "&12=2(CMS_HTMLHEAD-1,CMS_HTML-1)"
361: */
362: public function saveKeywords() {
363: $tmp_count = array();
364:
365: foreach ($this->_keywords as $keyword => $count) {
366: $tmp_count = preg_split('/[\s]/', trim($count));
367: $this->_debug('tmp_count', $tmp_count);
368:
369: $occurrence = count($tmp_count);
370: $tmp_count = array_unique($tmp_count);
371: $cms_types = implode(',', $tmp_count);
372: $index_string = '&' . $this->idart . '=' . $occurrence . '(' . $cms_types . ')';
373:
374: if (!array_key_exists($keyword, $this->_keywordsOld)) {
375: // if keyword is new, save index information
376: // $nextid = $this->db->nextid($this->cfg['tab']['keywords']);
377: $sql = "INSERT INTO " . $this->cfg['tab']['keywords'] . "
378: (keyword, " . $this->_place . ", idlang)
379: VALUES
380: ('" . $this->db->escape($keyword) . "', '" . $this->db->escape($index_string) . "', " . cSecurity::toInteger($this->lang) . ")";
381: } else {
382: // if keyword allready exists, create new index_string
383: if (preg_match("/&$this->idart=/", $this->_keywordsOld[$keyword])) {
384: $index_string = preg_replace("/&$this->idart=[0-9]+\([\w-,]+\)/", $index_string, $this->_keywordsOld[$keyword]);
385: } else {
386: $index_string = $this->_keywordsOld[$keyword] . $index_string;
387: }
388:
389: $sql = "UPDATE " . $this->cfg['tab']['keywords'] . "
390: SET " . $this->_place . " = '" . $index_string . "'
391: WHERE idlang='" . cSecurity::toInteger($this->lang) . "' AND keyword='" . $this->db->escape($keyword) . "'";
392: }
393: $this->_debug('sql', $sql);
394: $this->db->query($sql);
395: }
396: }
397:
398: /**
399: * if keywords don't occur in the article anymore, update index_string and
400: * delete keyword if necessary
401: */
402: public function deleteKeywords() {
403: foreach ($this->_keywordsDel as $key_del) {
404: $index_string = preg_replace("/&$this->idart=[0-9]+\([\w-,]+\)/", "", $this->_keywordsOld[$key_del]);
405:
406: if (strlen($index_string) == 0) {
407: // keyword is not referenced by any article
408: $sql = "DELETE FROM " . $this->cfg['tab']['keywords'] . "
409: WHERE idlang = " . cSecurity::toInteger($this->lang) . " AND keyword = '" . $this->db->escape($key_del) . "'";
410: } else {
411: $sql = "UPDATE " . $this->cfg['tab']['keywords'] . "
412: SET " . $this->_place . " = '" . $index_string . "'
413: WHERE idlang = " . cSecurity::toInteger($this->lang) . " AND keyword = '" . $this->db->escape($key_del) . "'";
414: }
415: $this->_debug('sql', $sql);
416: $this->db->query($sql);
417: }
418: }
419:
420: /**
421: * get the keywords of an article
422: */
423: public function getKeywords() {
424: $keys = implode("','", array_keys($this->_keywords));
425:
426: $sql = "SELECT
427: keyword, auto, self
428: FROM
429: " . $this->cfg['tab']['keywords'] . "
430: WHERE
431: idlang=" . cSecurity::toInteger($this->lang) . " AND
432: (keyword IN ('" . $keys . "') OR " . $this->_place . " REGEXP '&" . cSecurity::toInteger($this->idart) . "=')";
433:
434: $this->_debug('sql', $sql);
435:
436: $this->db->query($sql);
437:
438: $place = $this->_place;
439:
440: while ($this->db->nextRecord()) {
441: $this->_keywordsOld[$this->db->f('keyword')] = $this->db->f($place);
442: }
443: }
444:
445: /**
446: * remove special characters from index term
447: *
448: * @param $key string Keyword
449: * @return $key
450: */
451: public function removeSpecialChars($key) {
452: $aSpecialChars = array(
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: // for ($i = 127; $i < 192; $i++) {
489: // some other special characters
490: // $aSpecialChars[] = chr($i);
491: // }
492:
493: // TODO: The transformation of accented characters must depend on the
494: // selected encoding of the language of
495: // a client and should not be treated in this method.
496: // modified 2007-10-01, H. Librenz - added as hotfix for encoding
497: // problems (doesn't find any words with
498: // umlaut vowels in it since you turn on UTF-8 as language encoding)
499: $sEncoding = getEncodingByLanguage($this->db, $this->lang);
500:
501: if (strtolower($sEncoding) != 'iso-8859-2') {
502: $key = conHtmlentities($key, null, $sEncoding);
503: } else {
504: $key = htmlentities_iso88592($key);
505: }
506:
507: // $aUmlautMap = array(
508: // 'Ü' => 'ue',
509: // 'ü' => 'ue',
510: // 'Ä' => 'ae',
511: // 'ä' => 'ae',
512: // 'Ö' => 'oe',
513: // 'ö' => 'oe',
514: // 'ß' => 'ss'
515: // );
516:
517: // foreach ($aUmlautMap as $sUmlaut => $sMapped) {
518: // $key = str_replace($sUmlaut, $sMapped, $key);
519: // }
520:
521: $key = conHtmlEntityDecode($key);
522: $key = str_replace($aSpecialChars, '', $key);
523:
524: return $key;
525: }
526:
527: /**
528: * @modified 2008-04-17, Timo Trautmann - reverse function to
529: * removeSpecialChars
530: * (important for syntaxhighlighting searchterm in searchresults)
531: * adds umlauts to search term
532: *
533: * @param $key string Keyword
534: * @return $key
535: */
536: public function addSpecialUmlauts($key) {
537: $key = conHtmlentities($key, null, getEncodingByLanguage($this->db, $this->lang));
538: $aUmlautMap = array(
539: 'ue' => 'Ü',
540: 'ue' => 'ü',
541: 'ae' => 'Ä',
542: 'ae' => 'ä',
543: 'oe' => 'Ö',
544: 'oe' => 'ö',
545: 'ss' => 'ß'
546: );
547:
548: foreach ($aUmlautMap as $sUmlaut => $sMapped) {
549: $key = str_replace($sUmlaut, $sMapped, $key);
550: }
551:
552: $key = conHtmlEntityDecode($key);
553: return $key;
554: }
555:
556: /**
557: * set the array of stopwords which should not be indexed
558: *
559: * @param array $aStopwords
560: */
561: public function setStopwords($aStopwords) {
562: if (is_array($aStopwords) && count($aStopwords) > 0) {
563: $this->_stopwords = $aStopwords;
564: }
565: }
566:
567: /**
568: * set the cms types
569: */
570: public function setContentTypes() {
571: $sql = "SELECT type, idtype FROM " . $this->cfg['tab']['type'] . ' ';
572: $this->_debug('sql', $sql);
573: $this->db->query($sql);
574: while ($this->db->nextRecord()) {
575: $this->_cmsType[$this->db->f('type')] = $this->db->f('idtype');
576: $this->_cmsTypeSuffix[$this->db->f('idtype')] = substr($this->db->f('type'), 4, strlen($this->db->f('type')));
577: }
578: }
579:
580: /**
581: * set the cms_options array of cms types which should be treated special
582: */
583: public function setCmsOptions($cms_options) {
584: if (is_array($cms_options) && count($cms_options) > 0) {
585: foreach ($cms_options as $opt) {
586: $opt = strtoupper($opt);
587:
588: if (strlen($opt) > 0) {
589: if (!stristr($opt, 'cms_')) {
590: if (in_array($opt, $this->_cmsTypeSuffix)) {
591: $this->_cmsOptions[$opt] = 'CMS_' . $opt;
592: }
593: } else {
594: if (array_key_exists($opt, $this->_cmsType)) {
595: $this->_cmsOptions[$opt] = $opt;
596: }
597: }
598: }
599: }
600: } else {
601: $this->_cmsOptions = array();
602: }
603: }
604:
605: /**
606: * check if the current cms type is in the cms_options array
607: *
608: * @param $idtype
609: * @return boolean
610: */
611: public function checkCmsType($idtype) {
612: $idtype = strtoupper($idtype);
613: return (in_array($idtype, $this->_cmsOptions))? false : true;
614: }
615:
616: /**
617: *
618: * @return array the _cmsType property
619: */
620: public function getCmsType() {
621: return $this->_cmsType;
622: }
623:
624: /**
625: *
626: * @return array the _cmsTypeSuffix property
627: */
628: public function getCmsTypeSuffix() {
629: return $this->_cmsTypeSuffix;
630: }
631:
632: }
633:
634: /**
635: * CONTENIDO API - Search Object
636: *
637: * This object starts a indexed fulltext search
638: *
639: * TODO:
640: * The way to set the search options could be done much more better!
641: * The computation of the set of searchable articles should not be treated in
642: * this class.
643: * It is better to compute the array of searchable articles from the outside and
644: * to pass the array of searchable articles as parameter.
645: * Avoid foreach loops.
646: *
647: * Use object with
648: *
649: * $options = array('db' => 'regexp', // use db function regexp
650: * 'combine' => 'or'); // combine searchwords with or
651: *
652: * The range of searchable articles is by default the complete content which is
653: * online and not protected.
654: *
655: * With option 'searchable_articles' you can define your own set of searchable
656: * articles.
657: * If parameter 'searchable_articles' is set the options 'cat_tree',
658: * 'categories', 'articles', 'exclude', 'artspecs',
659: * 'protected', 'dontshowofflinearticles' don't have any effect.
660: *
661: * $options = array('db' => 'regexp', // use db function regexp
662: * 'combine' => 'or', // combine searchwords with or
663: * 'searchable_articles' => array(5, 6, 9, 13));
664: *
665: * One can define the range of searchable articles by setting the parameter
666: * 'exclude' to false which means the range of categories
667: * defined by parameter 'cat_tree' or 'categories' and the range of articles
668: * defined by parameter 'articles' is included.
669: *
670: * $options = array('db' => 'regexp', // use db function regexp
671: * 'combine' => 'or', // combine searchwords with or
672: * 'exclude' => false, // => searchrange specified in 'cat_tree', 'categories'
673: * and 'articles' is included
674: * 'cat_tree' => array(12), // tree with root 12 included
675: * 'categories' => array(100,111), // categories 100, 111 included
676: * 'articles' => array(33), // article 33 included
677: * 'artspecs' => array(2, 3), // array of article specifications => search only
678: * articles with these artspecs
679: * 'res_per_page' => 2, // results per page
680: * 'protected' => true); // => do not search articles or articles in categories
681: * which are offline or protected
682: * 'dontshowofflinearticles' => false); // => search offline articles or
683: * articles in categories which are offline
684: *
685: * You can build the complement of the range of searchable articles by setting
686: * the parameter 'exclude' to true which means the range of categories
687: * defined by parameter 'cat_tree' or 'categories' and the range of articles
688: * defined by parameter 'articles' is excluded from search.
689: *
690: * $options = array('db' => 'regexp', // use db function regexp
691: * 'combine' => 'or', // combine searchwords with or
692: * 'exclude' => true, // => searchrange specified in 'cat_tree', 'categories'
693: * and 'articles' is excluded
694: * 'cat_tree' => array(12), // tree with root 12 excluded
695: * 'categories' => array(100,111), // categories 100, 111 excluded
696: * 'articles' => array(33), // article 33 excluded
697: * 'artspecs' => array(2, 3), // array of article specifications => search only
698: * articles with these artspecs
699: * 'res_per_page' => 2, // results per page
700: * 'protected' => true); // => do not search articles or articles in categories
701: * which are offline or protected
702: * 'dontshowofflinearticles' => false); // => search offline articles or
703: * articles in categories which are offline
704: *
705: * $search = new Search($options);
706: *
707: * $cms_options = array("htmlhead", "html", "head", "text", "imgdescr", "link",
708: * "linkdescr");
709: * search only in these cms-types
710: * $search->setCmsOptions($cms_options);
711: *
712: * $search_result = $search->searchIndex($searchword, $searchwordex); // start
713: * search
714: *
715: * The search result structure has following form
716: * Array (
717: * [20] => Array (
718: * [CMS_HTML] => Array (
719: * [0] => 1
720: * [1] => 1
721: * [2] => 1
722: * )
723: * [keyword] => Array (
724: * [0] => content
725: * [1] => contenido
726: * [2] => wwwcontenidoorg
727: * )
728: * [search] => Array (
729: * [0] => con
730: * [1] => con
731: * [2] => con
732: * )
733: * [occurence] => Array (
734: * [0] => 1
735: * [1] => 5
736: * [2] => 1
737: * )
738: * [similarity] => 60
739: * )
740: * )
741: *
742: * The keys of the array are the article ID's found by search.
743: *
744: * Searching 'con' matches keywords 'content', 'contenido' and 'wwwcontenidoorg'
745: * in article with ID 20 in content type CMS_HTML[1].
746: * The search term occurs 7 times.
747: * The maximum similarity between searchterm and matching keyword is 60%.
748: *
749: * with $oSearchResults = new cSearchResult($search_result, 10);
750: * one can rank and display the results
751: *
752: * @package Core
753: * @subpackage Frontend_Search
754: */
755: class cSearch extends cSearchBaseAbstract {
756:
757: /**
758: * Instance of class Index
759: *
760: * @var object
761: */
762: protected $_index;
763:
764: /**
765: * array of available cms types
766: *
767: * @var array
768: */
769: protected $_cmsType = array();
770:
771: /**
772: * suffix of available cms types
773: *
774: * @var array
775: */
776: protected $_cmsTypeSuffix = array();
777:
778: /**
779: * the search words
780: *
781: * @var array
782: */
783: protected $_searchWords = array();
784:
785: /**
786: * the words which should be excluded from search
787: *
788: * @var array
789: */
790: protected $_searchWordsExclude = array();
791:
792: /**
793: * type of db search
794: * like => 'sql like', regexp => 'sql regexp'
795: *
796: * @var string
797: */
798: protected $_searchOption;
799:
800: /**
801: * logical combination of searchwords (and, or)
802: *
803: * @var string
804: */
805: protected $_searchCombination;
806:
807: /**
808: * array of searchable articles
809: *
810: * @var array
811: */
812: protected $_searchableArts = array();
813:
814: /**
815: * article specifications
816: *
817: * @var array
818: */
819: protected $_articleSpecs = array();
820:
821: /**
822: * If $protected = true => do not search articles which are offline or
823: * articles in catgeories which are offline (protected)
824: *
825: * @var boolean
826: */
827: protected $_protected;
828:
829: /**
830: * If $dontshowofflinearticles = false => search offline articles or
831: * articles in categories which are offline
832: *
833: * @var boolean
834: */
835: protected $_dontshowofflinearticles;
836:
837: /**
838: * If $exclude = true => the specified search range is excluded from search,
839: * otherwise included
840: *
841: * @var boolean
842: */
843: protected $_exclude;
844:
845: /**
846: * Array of article id's with information about cms-types, occurence of
847: * keyword/searchword, similarity .
848: *
849: * @var array
850: */
851: protected $_searchResult = array();
852:
853: /**
854: * Constructor
855: *
856: * @param array $options $options['db'] 'regexp' => DB search with REGEXP;
857: * 'like' => DB search with LIKE; 'exact' => exact match;
858: * $options['combine'] 'and', 'or' Combination of search words with
859: * AND, OR
860: * $options['exclude'] 'true' => searchrange specified in 'cat_tree',
861: * 'categories' and 'articles' is excluded; 'false' =>
862: * searchrange specified in 'cat_tree', 'categories' and
863: * 'articles' is included
864: * $options['cat_tree'] e.g. array(8) => The complete tree with root
865: * 8 is in/excluded from search
866: * $options['categories'] e.g. array(10, 12) => Categories 10, 12
867: * in/excluded
868: * $options['articles'] e.g. array(23) => Article 33 in/excluded
869: * $options['artspecs'] => e.g. array(2, 3) => search only articles
870: * with certain article specifications
871: * $options['protected'] 'true' => do not search articles which are
872: * offline (locked) or articles in catgeories which are offline
873: * (protected)
874: * $options['dontshowofflinearticles'] 'false' => search offline
875: * articles or articles in categories which are offline
876: * $options['searchable_articles'] array of article ID's which should
877: * be searchable
878: * @param cDb $db Optional database instance
879: */
880: public function __construct($options, $db = null) {
881: parent::__construct($db);
882:
883: $this->_index = new cSearchIndex($db);
884:
885: $this->_cmsType = $this->_index->cms_type;
886: $this->_cmsTypeSuffix = $this->_index->cms_type_suffix;
887:
888: $this->_searchOption = (array_key_exists('db', $options))? strtolower($options['db']) : 'regexp';
889: $this->_searchCombination = (array_key_exists('combine', $options))? strtolower($options['combine']) : 'or';
890: $this->_protected = (array_key_exists('protected', $options))? $options['protected'] : true;
891: $this->_dontshowofflinearticles = (array_key_exists('dontshowofflinearticles', $options))? $options['dontshowofflinearticles'] : false;
892: $this->_exclude = (array_key_exists('exclude', $options))? $options['exclude'] : true;
893: $this->_articleSpecs = (array_key_exists('artspecs', $options) && is_array($options['artspecs']))? $options['artspecs'] : array();
894: $this->_index->setCmsOptions($this->_cmsTypeSuffix);
895:
896: if (array_key_exists('searchable_articles', $options) && is_array($options['searchable_articles'])) {
897: $this->_searchableArts = $options['searchable_articles'];
898: } else {
899: $this->_searchableArts = $this->getSearchableArticles($options);
900: }
901:
902: // minimum similarity between searchword and keyword in percent
903: $this->intMinimumSimilarity = 50;
904: }
905:
906: /**
907: * indexed fulltext search
908: *
909: * @param string $searchwords The search words
910: * @param string $searchwords_exclude The words, which should be excluded
911: * from search
912: */
913: public function searchIndex($searchwords, $searchwords_exclude = '') {
914: if (strlen(trim($searchwords)) > 0) {
915: $this->_searchWords = $this->stripWords($searchwords);
916: } else {
917: return false;
918: }
919:
920: if (strlen(trim($searchwords_exclude)) > 0) {
921: $this->_searchWordsExclude = $this->stripWords($searchwords_exclude);
922: }
923:
924: $tmp_searchwords = array();
925: foreach ($this->_searchWords as $word) {
926: $wordEscaped = $this->db->escape($word);
927: if ($this->_searchOption == 'like') {
928: $wordEscaped = "'%" . $wordEscaped . "%'";
929: } elseif ($this->_searchOption == 'exact') {
930: $wordEscaped = "'" . $wordEscaped . "'";
931: }
932: $tmp_searchwords[] = $word;
933: }
934:
935: if (count($this->_searchWordsExclude) > 0) {
936: foreach ($this->_searchWordsExclude as $word) {
937: $wordEscaped = $this->db->escape($word);
938: if ($this->_searchOption == 'like') {
939: $wordEscaped = "'%" . $wordEscaped . "%'";
940: } elseif ($this->_searchOption == 'exact') {
941: $wordEscaped = "'" . $wordEscaped . "'";
942: }
943: $tmp_searchwords[] = $wordEscaped;
944: $this->_searchWords[] = $word;
945: }
946: }
947:
948: if ($this->_searchOption == 'regexp') {
949: // regexp search
950: $kwSql = "keyword REGEXP '" . implode('|', $tmp_searchwords) . "'";
951: } elseif ($this->_searchOption == 'like') {
952: // like search
953: $search_like = implode(" OR keyword LIKE ", $tmp_searchwords);
954: $kwSql = "keyword LIKE '" . $search_like;
955: } elseif ($this->_searchOption == 'exact') {
956: // exact match
957: $search_exact = implode(" OR keyword = ", $tmp_searchwords);
958: $kwSql = "keyword LIKE '" . $search_exact;
959: }
960:
961: $sql = "SELECT keyword, auto FROM " . $this->cfg['tab']['keywords'] . " WHERE idlang=" . cSecurity::toInteger($this->lang) . " AND " . $kwSql . " ";
962: $this->_debug('sql', $sql);
963: $this->db->query($sql);
964:
965: while ($this->db->nextRecord()) {
966:
967: $tmp_index_string = preg_split('/&/', $this->db->f('auto'), -1, PREG_SPLIT_NO_EMPTY);
968:
969: $this->_debug('index', $this->db->f('auto'));
970:
971: $tmp_index = array();
972: foreach ($tmp_index_string as $string) {
973: $tmp_string = preg_replace('/[=\(\)]/', ' ', $string);
974: $tmp_index[] = preg_split('/\s/', $tmp_string, -1, PREG_SPLIT_NO_EMPTY);
975: }
976: $this->_debug('tmp_index', $tmp_index);
977:
978: foreach ($tmp_index as $string) {
979: $artid = $string[0];
980:
981: // filter nonsearchable articles
982: if (in_array($artid, $this->_searchableArts)) {
983:
984: $cms_place = $string[2];
985: $keyword = $this->db->f('keyword');
986: $percent = 0;
987: $similarity = 0;
988: foreach ($this->_searchWords as $word) {
989: // computes similarity between searchword and keyword in
990: // percent
991: similar_text($word, $keyword, $percent);
992: if ($percent > $similarity) {
993: $similarity = $percent;
994: $searchword = $word;
995: }
996: }
997:
998: $tmp_cmstype = preg_split('/[,]/', $cms_place, -1, PREG_SPLIT_NO_EMPTY);
999: $this->_debug('tmp_cmstype', $tmp_cmstype);
1000:
1001: $tmp_cmstype2 = array();
1002: foreach ($tmp_cmstype as $type) {
1003: $tmp_cmstype2[] = preg_split('/-/', $type, -1, PREG_SPLIT_NO_EMPTY);
1004: }
1005: $this->_debug('tmp_cmstype2', $tmp_cmstype2);
1006:
1007: foreach ($tmp_cmstype2 as $type) {
1008: if (!$this->_index->checkCmsType($type[0])) {
1009: // search for specified cms-types
1010: if ($similarity >= $this->intMinimumSimilarity) {
1011: // include article into searchresult set only if
1012: // similarity between searchword and keyword is
1013: // big enough
1014: $this->_searchResult[$artid][$type[0]][] = $type[1];
1015: $this->_searchResult[$artid]['keyword'][] = $this->db->f('keyword');
1016: $this->_searchResult[$artid]['search'][] = $searchword;
1017: $this->_searchResult[$artid]['occurence'][] = $string[1];
1018: $this->_searchResult[$artid]['debug_similarity'][] = $percent;
1019: if ($similarity > $this->_searchResult[$artid]['similarity']) {
1020: $this->_searchResult[$artid]['similarity'] = $similarity;
1021: }
1022: }
1023: }
1024: }
1025: }
1026: }
1027: }
1028:
1029: if ($this->_searchCombination == 'and') {
1030: // all search words must appear in the article
1031: foreach ($this->_searchResult as $article => $val) {
1032: if (!count(array_diff($this->_searchWords, $val['search'])) == 0) {
1033: // $this->rank_structure[$article] = $rank[$article];
1034: unset($this->_searchResult[$article]);
1035: }
1036: }
1037: }
1038:
1039: if (count($this->_searchWordsExclude) > 0) {
1040: // search words to be excluded must not appear in article
1041: foreach ($this->_searchResult as $article => $val) {
1042: if (!count(array_intersect($this->_searchWordsExclude, $val['search'])) == 0) {
1043: // $this->rank_structure[$article] = $rank[$article];
1044: unset($this->_searchResult[$article]);
1045: }
1046: }
1047: }
1048:
1049: $this->_debug('$this->search_result', $this->_searchResult);
1050: $this->_debug('$this->searchable_arts', $this->_searchableArts);
1051:
1052: return $this->_searchResult;
1053: }
1054:
1055: /**
1056: *
1057: * @param $cms_options The cms-types (htmlhead, html, ...) which should
1058: * explicitly be searched
1059: */
1060: public function setCmsOptions($cms_options) {
1061: if (is_array($cms_options) && count($cms_options) > 0) {
1062: $this->_index->setCmsOptions($cms_options);
1063: }
1064: }
1065:
1066: /**
1067: *
1068: * @param $searchwords The search-words
1069: * @return Array of stripped search-words
1070: */
1071: public function stripWords($searchwords) {
1072: // remove backslash and html tags
1073: $searchwords = trim(strip_tags(stripslashes($searchwords)));
1074:
1075: // split the phrase by any number of commas or space characters
1076: $tmp_words = preg_split('/[\s,]+/', $searchwords);
1077:
1078: $tmp_searchwords = array();
1079:
1080: foreach ($tmp_words as $word) {
1081:
1082: $word = htmlentities($word, ENT_COMPAT, 'UTF-8');
1083: $word = (trim(strtolower($word)));
1084: $word = html_entity_decode($word, ENT_COMPAT, 'UTF-8');
1085:
1086: // $word =(trim(strtolower($word)));
1087: if (strlen($word) > 1) {
1088: $tmp_searchwords[] = $word;
1089: }
1090: }
1091:
1092: return array_unique($tmp_searchwords);
1093: }
1094:
1095: /**
1096: * Returns the category tree array.
1097: *
1098: * @param int $cat_start Root of a category tree
1099: * @return array Category Tree
1100: * @todo This is not the job for search, should be oursourced...
1101: */
1102: public function getSubTree($cat_start) {
1103: $sql = "SELECT
1104: B.idcat, B.parentid
1105: FROM
1106: " . $this->cfg['tab']['cat_tree'] . " AS A,
1107: " . $this->cfg['tab']['cat'] . " AS B,
1108: " . $this->cfg['tab']['cat_lang'] . " AS C
1109: WHERE
1110: A.idcat = B.idcat AND
1111: B.idcat = C.idcat AND
1112: C.idlang = '" . cSecurity::toInteger($this->lang) . "' AND
1113: B.idclient = '" . cSecurity::toInteger($this->client) . "'
1114: ORDER BY
1115: idtree";
1116: $this->_debug('sql', $sql);
1117: $this->db->query($sql);
1118:
1119: // $aSubCats = array();
1120: // $i = false;
1121: // while ($this->db->nextRecord()) {
1122: // if ($this->db->f('parentid') < $cat_start) {
1123: // // ending part of tree
1124: // $i = false;
1125: // }
1126: // if ($this->db->f('idcat') == $cat_start) {
1127: // // starting part of tree
1128: // $i = true;
1129: // }
1130: // if ($i == true) {
1131: // $aSubCats[] = $this->db->f('idcat');
1132: // }
1133: // }
1134:
1135: $aSubCats = array(
1136: $cat_start
1137: );
1138: while ($this->db->nextRecord()) {
1139: // ommit if cat is no child of any recognized descendant
1140: if (!in_array($this->db->f('parentid'), $aSubCats)) {
1141: continue;
1142: }
1143: // ommit if cat is already recognized (happens with $cat_start)
1144: if (in_array($this->db->f('idcat'), $aSubCats)) {
1145: continue;
1146: }
1147: // add cat as recognized descendant
1148: $aSubCats[] = $this->db->f('idcat');
1149: }
1150:
1151: return $aSubCats;
1152: }
1153:
1154: /**
1155: * Returns list of searchable article ids.
1156: *
1157: * @param array $search_range
1158: * @return array Articles in specified search range
1159: */
1160: public function getSearchableArticles($search_range) {
1161: $aCatRange = array();
1162: if (array_key_exists('cat_tree', $search_range) && is_array($search_range['cat_tree'])) {
1163: if (count($search_range['cat_tree']) > 0) {
1164: foreach ($search_range['cat_tree'] as $cat) {
1165: $aCatRange = array_merge($aCatRange, $this->getSubTree($cat));
1166: }
1167: }
1168: }
1169:
1170: if (array_key_exists('categories', $search_range) && is_array($search_range['categories'])) {
1171: if (count($search_range['categories']) > 0) {
1172: $aCatRange = array_merge($aCatRange, $search_range['categories']);
1173: }
1174: }
1175:
1176: $aCatRange = array_unique($aCatRange);
1177: $sCatRange = implode("','", $aCatRange);
1178:
1179: if (array_key_exists('articles', $search_range) && is_array($search_range['articles'])) {
1180: if (count($search_range['articles']) > 0) {
1181: $sArtRange = implode("','", $search_range['articles']);
1182: } else {
1183: $sArtRange = '';
1184: }
1185: }
1186:
1187: if ($this->_protected == true) {
1188: $sProtected = " C.public = 1 AND C.visible = 1 AND B.online = 1 ";
1189: } else {
1190: if ($this->_dontshowofflinearticles == true) {
1191: $sProtected = " C.visible = 1 AND B.online = 1 ";
1192: } else {
1193: $sProtected = " 1 ";
1194: }
1195: }
1196:
1197: if ($this->_exclude == true) {
1198: // exclude searchrange
1199: $sSearchRange = " A.idcat NOT IN ('" . $sCatRange . "') AND B.idart NOT IN ('" . $sArtRange . "') AND ";
1200: } else {
1201: // include searchrange
1202: if (strlen($sArtRange) > 0) {
1203: $sSearchRange = " A.idcat IN ('" . $sCatRange . "') AND B.idart IN ('" . $sArtRange . "') AND ";
1204: } else {
1205: $sSearchRange = " A.idcat IN ('" . $sCatRange . "') AND ";
1206: }
1207: }
1208:
1209: if (count($this->_articleSpecs) > 0) {
1210: $sArtSpecs = " B.artspec IN ('" . implode("','", $this->_articleSpecs) . "') AND ";
1211: } else {
1212: $sArtSpecs = '';
1213: }
1214:
1215: $sql = "SELECT
1216: A.idart
1217: FROM
1218: " . $this->cfg["tab"]["cat_art"] . " as A,
1219: " . $this->cfg["tab"]["art_lang"] . " as B,
1220: " . $this->cfg["tab"]["cat_lang"] . " as C
1221: WHERE
1222: " . $sSearchRange . "
1223: B.idlang = '" . cSecurity::toInteger($this->lang) . "' AND
1224: C.idlang = '" . cSecurity::toInteger($this->lang) . "' AND
1225: A.idart = B.idart AND
1226: B.searchable = 1 AND
1227: A.idcat = C.idcat AND
1228: " . $sArtSpecs . "
1229: " . $sProtected . " ";
1230: $this->_debug('sql', $sql);
1231: $this->db->query($sql);
1232:
1233: $aIdArts = array();
1234: while ($this->db->nextRecord()) {
1235: $aIdArts[] = $this->db->f('idart');
1236: }
1237: return $aIdArts;
1238: }
1239:
1240: /**
1241: * Fetch all article specifications which are online,
1242: *
1243: * @return array Array of article specification Ids
1244: */
1245: public function getArticleSpecifications() {
1246: $sql = "SELECT
1247: idartspec
1248: FROM
1249: " . $this->cfg['tab']['art_spec'] . "
1250: WHERE
1251: client = " . cSecurity::toInteger($this->client) . " AND
1252: lang = " . cSecurity::toInteger($this->lang) . " AND
1253: online = 1 ";
1254: $this->_debug('sql', $sql);
1255: $this->db->query($sql);
1256: $aArtspec = array();
1257: while ($this->db->nextRecord()) {
1258: $aArtspec[] = $this->db->f('idartspec');
1259: }
1260: return $aArtspec;
1261: }
1262:
1263: /**
1264: * Set article specification
1265: *
1266: * @param int $iArtspecID
1267: */
1268: public function setArticleSpecification($iArtspecID) {
1269: $this->_articleSpecs[] = $iArtspecID;
1270: }
1271:
1272: /**
1273: * Add all article specifications matching name of article specification
1274: * (client dependent but language independent)
1275: *
1276: * @param string $sArtSpecName
1277: */
1278: public function addArticleSpecificationsByName($sArtSpecName) {
1279: if (!isset($sArtSpecName) || strlen($sArtSpecName) == 0) {
1280: return false;
1281: }
1282:
1283: $sql = "SELECT
1284: idartspec
1285: FROM
1286: " . $this->cfg['tab']['art_spec'] . "
1287: WHERE
1288: client = " . cSecurity::toInteger($this->client) . " AND
1289: artspec = '" . $this->db->escape($sArtSpecName) . "' ";
1290: $this->_debug('sql', $sql);
1291: $this->db->query($sql);
1292: while ($this->db->nextRecord()) {
1293: $this->_articleSpecs[] = $this->db->f('idartspec');
1294: }
1295: }
1296:
1297: }
1298:
1299: /**
1300: * CONTENIDO API - SearchResult Object
1301: *
1302: * This object ranks and displays the result of the indexed fulltext search.
1303: * If you are not comfortable with this API feel free to use your own methods to
1304: * display the search results.
1305: * The search result is basically an array with article ID's.
1306: *
1307: * If $search_result = $search->searchIndex($searchword, $searchwordex);
1308: *
1309: * use object with
1310: *
1311: * $oSearchResults = new cSearchResult($search_result, 10);
1312: *
1313: * $oSearchResults->setReplacement('<span style="color:red">', '</span>'); //
1314: * html-tags to emphasize the located searchwords
1315: *
1316: * $num_res = $oSearchResults->getNumberOfResults();
1317: * $num_pages = $oSearchResults->getNumberOfPages();
1318: * $res_page = $oSearchResults->getSearchResultPage(1); // first result page
1319: * foreach ($res_page as $key => $val) {
1320: * $headline = $oSearchResults->getSearchContent($key, 'HTMLHEAD');
1321: * $first_headline = $headline[0];
1322: * $text = $oSearchResults->getSearchContent($key, 'HTML');
1323: * $first_text = $text[0];
1324: * $similarity = $oSearchResults->getSimilarity($key);
1325: * $iOccurrence = $oSearchResults->getOccurrence($key);
1326: * }
1327: *
1328: * @package Core
1329: * @subpackage Frontend_Search
1330: *
1331: */
1332: class cSearchResult extends cSearchBaseAbstract {
1333:
1334: /**
1335: * Instance of class Index
1336: *
1337: * @var object
1338: */
1339: protected $_index;
1340:
1341: /**
1342: * Number of results
1343: *
1344: * @var int
1345: */
1346: protected $_results;
1347:
1348: /**
1349: * Number of result pages
1350: *
1351: * @var int
1352: */
1353: protected $_pages;
1354:
1355: /**
1356: * Current result page
1357: *
1358: * @var int
1359: */
1360: protected $_resultPage;
1361:
1362: /**
1363: * Results per page to display
1364: *
1365: * @var int
1366: */
1367: protected $_resultPerPage;
1368:
1369: /**
1370: * Array of html-tags to emphasize the searchwords
1371: *
1372: * @var array
1373: */
1374: protected $_replacement = array();
1375:
1376: /**
1377: * Array of article id's with ranking information
1378: *
1379: * @var array
1380: */
1381: protected $_rankStructure = array();
1382:
1383: /**
1384: * Array of result-pages with array's of article id's
1385: *
1386: * @var array
1387: */
1388: protected $_orderedSearchResult = array();
1389:
1390: /**
1391: * Array of article id's with information about cms-types, occurence of
1392: * keyword/searchword, similarity .
1393: *
1394: *
1395: * @var array
1396: */
1397: protected $_searchResult = array();
1398:
1399: /**
1400: * Compute ranking factor for each search result and order the search
1401: * results by ranking factor
1402: * NOTE: The ranking factor is the sum of occurences of matching searchterms
1403: * weighted by similarity (in %) between searchword
1404: * and matching word in the article.
1405: * TODO: One can think of more sophisticated ranking strategies. One could
1406: * use the content type information for example
1407: * because a matching word in the headline (CMS_HEADLINE[1]) could be
1408: * weighted more than a matching word in the text (CMS_HTML[1]).
1409: *
1410: * @param array $search_result List of article ids
1411: * @param int $result_per_page Number of items per page
1412: * @param cDb $oDB Optional db instance
1413: * @param bool $bDebug Optional flag to enable debugging
1414: */
1415: public function __construct($search_result, $result_per_page, $oDB = null, $bDebug = false) {
1416: parent::__construct($oDB, $bDebug);
1417:
1418: $this->_index = new cSearchIndex($oDB);
1419:
1420: $this->_searchResult = $search_result;
1421: $this->_debug('$this->search_result', $this->_searchResult);
1422:
1423: $this->_resultPerPage = $result_per_page;
1424: $this->_results = count($this->_searchResult);
1425:
1426: // compute ranking factor for each search result
1427: foreach ($this->_searchResult as $article => $val) {
1428: $this->_rankStructure[$article] = $this->getOccurrence($article) * ($this->getSimilarity($article) / 100);
1429: }
1430: $this->_debug('$this->rank_structure', $this->_rankStructure);
1431:
1432: $this->setOrderedSearchResult($this->_rankStructure, $this->_resultPerPage);
1433: $this->_pages = count($this->_orderedSearchResult);
1434: $this->_debug('$this->ordered_search_result', $this->_orderedSearchResult);
1435: }
1436:
1437: /**
1438: *
1439: * @param $ranked_search
1440: * @param $result_per_page
1441: */
1442: public function setOrderedSearchResult($ranked_search, $result_per_page) {
1443: asort($ranked_search);
1444:
1445: $sorted_rank = array_reverse($ranked_search, true);
1446:
1447: if (isset($result_per_page) && $result_per_page > 0) {
1448: $split_result = array_chunk($sorted_rank, $result_per_page, true);
1449: $this->_orderedSearchResult = $split_result;
1450: } else {
1451: $this->_orderedSearchResult[] = $sorted_rank;
1452: }
1453: }
1454:
1455: /**
1456: *
1457: * @param $cms_type
1458: * @param $art_id int Id of an article
1459: * @return string Content of an article, specified by it's content type
1460: */
1461: public function getContent($art_id, $cms_type, $id = 0) {
1462: $article = new cApiArticleLanguage();
1463: $article->loadByArticleAndLanguageId($art_id, $this->lang, true);
1464: return $article->getContent($cms_type, $id);
1465: }
1466:
1467: /**
1468: *
1469: * @param $cms_type string Content type
1470: * @param $art_id int Id of an article
1471: * @return string Content of an article in search result, specified by its
1472: * type
1473: */
1474: public function getSearchContent($art_id, $cms_type, $cms_nr = NULL) {
1475: $cms_type = strtoupper($cms_type);
1476: if (strlen($cms_type) > 0) {
1477: if (!stristr($cms_type, 'cms_')) {
1478: if (in_array($cms_type, $this->_index->getCmsTypeSuffix())) {
1479: $cms_type = 'CMS_' . $cms_type;
1480: }
1481: } else {
1482: if (!array_key_exists($cms_type, $this->_index->getCmsType())) {
1483: return array();
1484: }
1485: }
1486: }
1487:
1488: $article = new cApiArticleLanguage();
1489: $article->loadByArticleAndLanguageId($art_id, $this->lang, true);
1490: $content = array();
1491: if (isset($this->_searchResult[$art_id][$cms_type])) {
1492: // if searchword occurs in cms_type
1493: $search_words = $this->_searchResult[$art_id]['search'];
1494: $search_words = array_unique($search_words);
1495:
1496: $id_type = $this->_searchResult[$art_id][$cms_type];
1497: $id_type = array_unique($id_type);
1498:
1499: if (isset($cms_nr) && is_numeric($cms_nr)) {
1500: // get content of cms_type[cms_nr]
1501: // build consistent escaped string(Timo Trautmann) 2008-04-17
1502: $cms_content = conHtmlentities(conHtmlEntityDecode(strip_tags($article->getContent($cms_type, $cms_nr))));
1503: if (count($this->_replacement) == 2) {
1504: foreach ($search_words as $word) {
1505: // build consistent escaped string, replace ae ue ..
1506: // with original html entities (Timo Trautmann)
1507: // 2008-04-17
1508: $word = conHtmlentities(conHtmlEntityDecode($this->_index->addSpecialUmlauts($word)));
1509: $match = array();
1510: preg_match("/$word/i", $cms_content, $match);
1511: if (isset($match[0])) {
1512: $pattern = $match[0];
1513: $replacement = $this->_replacement[0] . $pattern . $this->_replacement[1];
1514: $cms_content = preg_replace("/$pattern/i", $replacement, $cms_content); // emphasize
1515: // located
1516: // searchwords
1517: }
1518: }
1519: }
1520: $content[] = htmlspecialchars_decode($cms_content);
1521: } else {
1522: // get content of cms_type[$id], where $id are the cms_type
1523: // numbers found in search
1524: foreach ($id_type as $id) {
1525: $cms_content = strip_tags($article->getContent($cms_type, $id));
1526:
1527: if (count($this->_replacement) == 2) {
1528: foreach ($search_words as $word) {
1529: preg_match("/$word/i", $cms_content, $match);
1530: if (isset($match[0])) {
1531: $pattern = $match[0];
1532: $replacement = $this->_replacement[0] . $pattern . $this->_replacement[1];
1533: $cms_content = preg_replace("/$pattern/i", $replacement, $cms_content); // emphasize
1534: // located
1535: // searchwords
1536: }
1537: }
1538: }
1539: $content[] = $cms_content;
1540: }
1541: }
1542: } else {
1543: // searchword was not found in cms_type
1544: if (isset($cms_nr) && is_numeric($cms_nr)) {
1545: $content[] = strip_tags($article->getContent($cms_type, $cms_nr));
1546: } else {
1547: $art_content = $article->getContent($cms_type);
1548: if (count($art_content) > 0) {
1549: foreach ($art_content as $val) {
1550: $content[] = strip_tags($val);
1551: }
1552: }
1553: }
1554: }
1555: return $content;
1556: }
1557:
1558: /**
1559: * Returns articles in page.
1560: *
1561: * @param int $page_id
1562: * @return array Articles in page $page_id
1563: */
1564: public function getSearchResultPage($page_id) {
1565: $this->_resultPage = $page_id;
1566: $result_page = $this->_orderedSearchResult[$page_id - 1];
1567: return $result_page;
1568: }
1569:
1570: /**
1571: * Returns number of result pages
1572: *
1573: * @return int
1574: */
1575: public function getNumberOfPages() {
1576: return $this->_pages;
1577: }
1578:
1579: /**
1580: * Returns number of results
1581: *
1582: * @return int
1583: */
1584: public function getNumberOfResults() {
1585: return $this->_results;
1586: }
1587:
1588: /**
1589: *
1590: * @param $art_id int Id of an article
1591: * @return int Similarity between searchword and matching word in article
1592: */
1593: public function getSimilarity($art_id) {
1594: return $this->_searchResult[$art_id]['similarity'];
1595: }
1596:
1597: /**
1598: *
1599: * @param $art_id int Id of an article
1600: * @return Number of matching searchwords found in article
1601: */
1602: public function getOccurrence($art_id) {
1603: $aOccurence = $this->_searchResult[$art_id]['occurence'];
1604: $iSumOfOccurence = 0;
1605: for ($i = 0; $i < count($aOccurence); $i++) {
1606: $iSumOfOccurence += $aOccurence[$i];
1607: }
1608:
1609: return $iSumOfOccurence;
1610: }
1611:
1612: /**
1613: *
1614: * @param string $rep1 The opening html-tag to emphasize the searchword e.g.
1615: * '<b>'
1616: * @param string $rep2 The closing html-tag e.g. '</b>'
1617: * @return void
1618: */
1619: public function setReplacement($rep1, $rep2) {
1620: if (strlen(trim($rep1)) > 0 && strlen(trim($rep2)) > 0) {
1621: $this->_replacement[] = $rep1;
1622: $this->_replacement[] = $rep2;
1623: }
1624: }
1625:
1626: /**
1627: *
1628: * @param $artid
1629: * @return int Category Id
1630: * @todo Is not job of search, should be outsourced!
1631: */
1632: public function getArtCat($artid) {
1633: $sql = "SELECT idcat FROM " . $this->cfg['tab']['cat_art'] . "
1634: WHERE idart = " . cSecurity::toInteger($artid) . " ";
1635: $this->db->query($sql);
1636: if ($this->db->nextRecord()) {
1637: return $this->db->f('idcat');
1638: }
1639: }
1640:
1641: }
1642: