      1: <?php
      2:
      3: // include the config file
      4: if(!isset($config)) {
      5:     require_once '../util/AlphaConfig.inc';
      6:     $config = AlphaConfig::getInstance();
      7:
      8:     require_once $config->get('app.root').'alpha/util/AlphaAutoLoader.inc';
      9: }
     10:
     11: /**
     12:  *
     13:  * Generic tag-based search engine controller
     14:  *
     15:  * @package alpha::controller
     16:  * @since 1.0
     17:  * @author John Collins <dev@alphaframework.org>
     18:  * @version $Id: Search.php 1652 2013-02-26 15:13:13Z alphadevx $
     19:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     20:  * @copyright Copyright (c) 2013, John Collins (founder of Alpha Framework).
     21:  * All rights reserved.
     22:  *
     23:  * <pre>
     24:  * Redistribution and use in source and binary forms, with or
     25:  * without modification, are permitted provided that the
     26:  * following conditions are met:
     27:  *
     28:  * * Redistributions of source code must retain the above
     29:  *   copyright notice, this list of conditions and the
     30:  *   following disclaimer.
     31:  * * Redistributions in binary form must reproduce the above
     32:  *   copyright notice, this list of conditions and the
     33:  *   following disclaimer in the documentation and/or other
     34:  *   materials provided with the distribution.
     35:  * * Neither the name of the Alpha Framework nor the names
     36:  *   of its contributors may be used to endorse or promote
     37:  *   products derived from this software without specific
     38:  *   prior written permission.
     39:  *
     40:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
     41:  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
     42:  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     43:  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     44:  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
     45:  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     46:  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     47:  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     48:  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     49:  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     50:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
     51:  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     52:  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     53:  * </pre>
     54:  *
     55:  */
     56: class Search extends AlphaController implements AlphaControllerInterface {
     57:     /**
     58:      * Trace logger
     59:      *
     60:      * @var Logger
     61:      * @since 1.0
     62:      */
     63:     private static $logger = null;
     64:
     65:     /**
     66:      * The start number for list pageination
     67:      *
     68:      * @var integer
     69:      * @since 1.0
     70:      */
     71:     protected $startPoint;
     72:
     73:     /**
     74:      * The result count from the search
     75:      *
     76:      * @var integer
     77:      * @since 1.0
     78:      */
     79:     private $resultCount = 0;
     80:
     81:     /**
     82:      * The search query supplied
     83:      *
     84:      * @var string
     85:      * @since 1.0
     86:      */
     87:     private $query;
     88:
     89:     /**
     90:      * constructor to set up the object
     91:      *
     92:      * @param string $visibility The name of the rights group that can access this controller.
     93:      * @since 1.0
     94:      */
     95:     public function __construct($visibility='Public') {
     96:         self::$logger = new Logger('Search');
     97:         self::$logger->debug('>>__construct(visibility=['.$visibility.'])');
     98:
     99:         global $config;
    100:
    101:         // ensure that the super class constructor is called, indicating the rights group
    102:         parent::__construct($visibility);
    103:
    104:         self::$logger->debug('<<__construct');
    105:     }
    106:
    107:     /**
    108:      * Handle GET requests
    109:      *
    110:      * @param array $params
    111:      * @since 1.0
    112:      * @throws IllegalArguementException
    113:      */
    114:     public function doGET($params) {
    115:         self::$logger->debug('>>doGET($params=['.var_export($params, true).'])');
    116:
    117:         if (isset($params['start']) ? $this->startPoint = $params['start']: $this->startPoint = 0);
    118:
    119:         global $config;
    120:
    121:         $KPI = new AlphaKPI('search');
    122:
    123:         if(isset($params['q'])) {
    124:
    125:             $this->query = $params['q'];
    126:
    127:             // replace any %20 on the URL with spaces
    128:             $params['q'] = str_replace('%20', ' ', $params['q']);
    129:
    130:             $this->setTitle('Search results - '.$params['q']);
    131:             echo AlphaView::displayPageHead($this);
    132:
    133:             // log the user's search query in a log file
    134:             $log = new LogFile($config->get('app.file.store.dir').'logs/search.log');
    135:             $log->writeLine(array($params['q'], date('Y-m-d H:i:s'), $_SERVER['HTTP_USER_AGENT'], $_SERVER['REMOTE_ADDR']));
    136:
    137:             $KPI->logStep('log search query');
    138:
    139:             // if a BO name is provided, only search tags on that class, otherwise search all BOs
    140:             if(isset($params['bo']))
    141:                 $BOs = array($params['bo']);
    142:             else
    143:                 $BOs = AlphaDAO::getBOClassNames();
    144:
    145:             try {
    146:                 foreach($BOs as $BO) {
    147:                     AlphaDAO::loadClassDef($BO);
    148:                     $temp = new $BO;
    149:
    150:                     if($temp->isTagged()) {
    151:                         $BOIDs = $this->doTagSearch($params['q'], $BO);
    152:
    153:                         $this->resultCount += count($BOIDs);
    154:
    155:                         $KPI->logStep('built array of matching OIDs');
    156:
    157:                         // sort the BO IDs based on tag frequency weight
    158:                         arsort($BOIDs);
    159:
    160:                         // render the list view for each BO
    161:                         $this->renderResultList($BOIDs, $BO, $params['q']);
    162:
    163:                         AlphaDAO::disconnect();
    164:                     }
    165:                 }
    166:             }catch(Exception $e) {
    167:                 self::$logger->error($e->getMessage());
    168:                 throw new IllegalArguementException('Error occured while searching for: ['.$params['q'].']');
    169:             }
    170:         }else{
    171:             $this->setTitle('Search results');
    172:             echo AlphaView::displayPageHead($this);
    173:             self::$logger->debug('No search query provided!');
    174:         }
    175:
    176:         echo AlphaView::displayPageFoot($this);
    177:
    178:         $KPI->log();
    179:
    180:         self::$logger->debug('<<doGET');
    181:     }
    182:
    183:     /**
    184:      * Searches for tags on the given BO type matching the query provided
    185:      *
    186:      * @param string $query
    187:      * @param string $BOName
    188:      * @param array $BOIDs
    189:      * @return array
    190:      * @since 1.1
    191:      */
    192:     protected function doTagSearch($query, $BOName, $BOIDs=array()) {
    193:         // explode the user's query into a set of tokenized transient TagObjects
    194:         $queryTags = TagObject::tokenize($query, '', '', false);
    195:         $matchingTags = array();
    196:
    197:         // load TagObjects from the DB where content equals the content of one of our transient TagObjects
    198:         foreach($queryTags as $queryTag) {
    199:             $tags = $queryTag->loadAllByAttribute('content', $queryTag->get('content'));
    200:             $matchingTags = array_merge($matchingTags, $tags);
    201:         }
    202:
    203:         self::$logger->debug('There are ['.count($matchingTags).'] TagObjects matching the query ['.$query.']');
    204:
    205:         /*
    206:          * Build an array of BOs for the matching tags from the DB:
    207:          * array key = BO ID
    208:          * array value = weight (the amount of tags matching the BO)
    209:          */
    210:         foreach($matchingTags as $tag) {
    211:             if($tag->get('taggedClass') == $BOName) {
    212:
    213:                 if(isset($BOIDs[$tag->get('taggedOID')])) {
    214:                     // increment the weight if the same BO is tagged more than once
    215:                     $weight = intval($BOIDs[$tag->get('taggedOID')]) + 1;
    216:                     $BOIDs[$tag->get('taggedOID')] = $weight;
    217:                 }else{
    218:                     $BOIDs[$tag->get('taggedOID')] = 1;
    219:                 }
    220:
    221:                 self::$logger->debug('Found BO ['.$tag->get('taggedOID').'] has weight ['.$BOIDs[$tag->get('taggedOID')].']');
    222:             }
    223:         }
    224:
    225:         return $BOIDs;
    226:     }
    227:
    228:     /**
    229:      * Renders the search result list
    230:      *
    231:      * @param array $BOIDs
    232:      * @param string $BO
    233:      * @param string $query
    234:      * @param bool $showTags
    235:      * @param bool $showScore
    236:      * @since 1.0
    237:      */
    238:     protected function renderResultList($BOIDs, $BO, $query='', $showTags=true, $showScore=false) {
    239:         global $config;
    240:
    241:         // used to track when our pagination range ends
    242:         $end = ($this->startPoint+$config->get('app.list.page.amount'));
    243:         // used to track how many results have been displayed or skipped from the pagination range
    244:         $displayedCount = 0;
    245:
    246:         if(!empty($query))
    247:             echo '<h2>Displaying results for &quot;'.$query.'&quot;</h2>';
    248:
    249:         foreach(array_keys($BOIDs) as $oid) {
    250:             try {
    251:                 // if we have reached the end of the pagination range then break out
    252:                 if($displayedCount == $end)
    253:                     break;
    254:
    255:                 // if our display count is >= the start but < the end...
    256:                 if($displayedCount >= $this->startPoint) {
    257:                     $temp = new $BO;
    258:                     $temp->load($oid);
    259:
    260:                     if($temp instanceof ArticleObject && $temp->get('published') == false){
    261:                         $this->resultCount--;
    262:                     }else{
    263:                         $view = AlphaView::getInstance($temp);
    264:                         echo $view->listView();
    265:
    266:                         if($showTags) {
    267:                             $tags = $temp->getPropObject('tags')->getRelatedObjects();
    268:
    269:                             if(count($tags) > 0) {
    270:                                 echo '<p>Tags: ';
    271:
    272:                                 $queryTerms = explode(' ', strtolower($query));
    273:
    274:                                 foreach($tags as $tag) {
    275:                                     echo (in_array($tag->get('content'), $queryTerms) ? '<strong>'.$tag->get('content').' </strong>' : $tag->get('content').' ');
    276:                                 }
    277:
    278:                                 echo '</p>';
    279:                             }
    280:                         }
    281:
    282:                         if($showScore) {
    283:                             $score = $BOIDs[$oid];
    284:
    285:                             echo '<p>Score: <strong>'.$score.'</strong></p>';
    286:                         }
    287:                     }
    288:                 }
    289:
    290:                 $displayedCount++;
    291:             }catch(BONotFoundException $e) {
    292:                 self::$logger->warn('Orpaned TagObject detected pointing to a non-existant BO of OID ['.$oid.'] and type ['.$BO.'].');
    293:             }
    294:         }
    295:     }
    296:
    297:     /**
    298:      * Handle POST requests
    299:      *
    300:      * @param array $params
    301:      * @since 1.0
    302:      */
    303:     public function doPOST($params) {
    304:         self::$logger->debug('>>doPOST($params=['.var_export($params, true).'])');
    305:
    306:         self::$logger->debug('<<doPOST');
    307:     }
    308:
    309:     /**
    310:      * Displays a search form on the top of the page
    311:      *
    312:      * @return string
    313:      * @since 1.0
    314:      */
    315:     public function after_displayPageHead_callback() {
    316:         global $config;
    317:
    318:         $html = '<div align="center"><form method="GET" id="search_form" onsubmit="document.location = \''.$config->get('app.url').'search/q/\'+document.getElementById(\'q\').value; return false;">';
    319:         $html .= 'Search for: <input type="text" size="80" name="q" id="q"/>&nbsp;';
    320:         $button = new Button('document.location = \''.$config->get('app.url').'search/q/\'+document.getElementById(\'q\').value', 'Search', 'searchButton');
    321:         $html .= $button->render();
    322:         $html .= '</form></div>';
    323:
    324:         return $html;
    325:     }
    326:
    327:     /**
    328:      * Method to display the page footer with pageination links
    329:      *
    330:      * @return string
    331:      * @since 1.0
    332:      */
    333:     public function before_displayPageFoot_callback() {
    334:         $html = $this->renderPageLinks();
    335:
    336:         $html .= '<br>';
    337:
    338:         return $html;
    339:     }
    340:
    341:     /**
    342:      * Method for rendering the pagination links
    343:      *
    344:      * @return string
    345:      * @since 1.0
    346:      */
    347:     protected function renderPageLinks() {
    348:         global $config;
    349:
    350:         $html = '';
    351:
    352:         $end = ($this->startPoint+$config->get('app.list.page.amount'));
    353:
    354:         if($end > $this->resultCount)
    355:             $end = $this->resultCount;
    356:
    357:         if($this->resultCount > 0) {
    358:             $html .= '<p align="center">Displaying '.($this->startPoint+1).' to '.$end.' of <strong>'.$this->resultCount.'</strong>.&nbsp;&nbsp;';
    359:         }else{
    360:             if(!empty($this->query))
    361:                 $html .= AlphaView::displayUpdateMessage('There were no search results for your query.');
    362:         }
    363:
    364:         if ($this->startPoint > 0) {
    365:             // handle secure URLs
    366:             if(isset($_GET['tk']))
    367:                 $html .= '<a href="'.FrontController::generateSecureURL('act=Search&q='.$this->query.'&start='.($this->startPoint-$config->get('app.list.page.amount'))).'">&lt;&lt;-Previous</a>&nbsp;&nbsp;';
    368:             else
    369:                 $html .= '<a href="'.$config->get('app.url').'search/q/'.$this->query.'/start/'.($this->startPoint-$config->get('app.list.page.amount')).'">&lt;&lt;-Previous</a>&nbsp;&nbsp;';
    370:         }elseif($this->resultCount > $config->get('app.list.page.amount')){
    371:             $html .= '&lt;&lt;-Previous&nbsp;&nbsp;';
    372:         }
    373:         $page = 1;
    374:         for ($i = 0; $i < $this->resultCount; $i+=$config->get('app.list.page.amount')) {
    375:             if($i != $this->startPoint) {
    376:                 // handle secure URLs
    377:                 if(isset($_GET['tk']))
    378:                     $html .= '&nbsp;<a href="'.FrontController::generateSecureURL('act=Search&q='.$this->query.'&start='.$i).'">'.$page.'</a>&nbsp;';
    379:                 else
    380:                     $html .= '&nbsp;<a href="'.$config->get('app.url').'search/q/'.$this->query.'/start/'.$i.'">'.$page.'</a>&nbsp;';
    381:             }elseif($this->resultCount > $config->get('app.list.page.amount')){
    382:                 $html .= '&nbsp;'.$page.'&nbsp;';
    383:             }
    384:             $page++;
    385:         }
    386:         if ($this->resultCount > $end) {
    387:             // handle secure URLs
    388:             if(isset($_GET['tk']))
    389:                 $html .= '&nbsp;&nbsp;<a href="'.FrontController::generateSecureURL('act=Search&q='.$this->query.'&start='.($this->startPoint+$config->get('app.list.page.amount'))).'">Next-&gt;&gt;</a>';
    390:             else
    391:                 $html .= '&nbsp;&nbsp;<a href="'.$config->get('app.url').'search/q/'.$this->query.'/start/'.($this->startPoint+$config->get('app.list.page.amount')).'">Next-&gt;&gt;</a>';
    392:         }elseif($this->resultCount > $config->get('app.list.page.amount')){
    393:             $html .= '&nbsp;&nbsp;Next-&gt;&gt;';
    394:         }
    395:         $html .= '</p>';
    396:
    397:         return $html;
    398:     }
    399: }
    400:
    401: // now build the new controller
    402: if(basename($_SERVER['PHP_SELF']) == 'Search.php') {
    403:     $controller = new Search();
    404:
    405:     if(!empty($_POST)) {
    406:         $controller->doPOST($_REQUEST);
    407:     }else{
    408:         $controller->doGET($_GET);
    409:     }
    410: }
    411:
    412: ?>