1<?php 2 3namespace Bolt; 4 5use Bolt\Translation\Translator as Trans; 6use Symfony\Component\Finder\Finder; 7 8/** 9 * Simple search implementation for the Bolt backend. 10 * 11 * TODO: 12 * - permissions 13 * - a config.yml for search options 14 * 15 * @author Xiao-HuTai, xiao@twokings.nl 16 */ 17class Omnisearch 18{ 19 const OMNISEARCH_LANDINGPAGE = 99999; 20 const OMNISEARCH_CONTENTTYPE = 9999; 21 const OMNISEARCH_MENUITEM = 5000; 22 const OMNISEARCH_EXTENSION = 3000; 23 const OMNISEARCH_CONTENT = 2000; 24 const OMNISEARCH_FILE = 1000; 25 26 private $showNewContenttype = true; 27 private $showViewContenttype = true; 28 private $showConfiguration = true; 29 private $showMaintenance = true; 30 private $showExtensions = true; 31 private $showFiles = true; 32 private $showRecords = true; 33 34 // Show the option to the landing page for search results. 35 private $showLandingpage = true; 36 37 private $app; 38 private $data; 39 40 public function __construct(Application $app) 41 { 42 $this->app = $app; 43 44 $this->initialize(); 45 } 46 47 public function initialize() 48 { 49 $this->initContenttypes(); 50 $this->initMenuitems(); 51 $this->initExtensions(); 52 } 53 54 private function initContenttypes() 55 { 56 $contenttypes = $this->app['config']->get('contenttypes'); 57 58 foreach ($contenttypes as $key => $value) { 59 $pluralname = $value['name']; 60 $singularname = $value['singular_name']; 61 $slug = $value['slug']; 62 $keywords = array( 63 $pluralname, 64 $singularname, 65 $slug, 66 $key, 67 ); 68 69 $viewContenttype = Trans::__('contenttypes.generic.view', array('%contenttypes%' => $key)); 70 $newContenttype = Trans::__('contenttypes.generic.new', array('%contenttype%' => $key)); 71 72 if ($this->showViewContenttype) { 73 $viewKeywords = $keywords; 74 $viewKeywords[] = $viewContenttype; 75 $viewKeywords[] = 'View ' . $pluralname; 76 77 $this->register( 78 array( 79 'keywords' => $viewKeywords, 80 'label' => $viewContenttype, 81 'description' => '', 82 'priority' => self::OMNISEARCH_CONTENTTYPE, 83 'path' => $this->app->generatePath('overview', array('contenttypeslug' => $slug)), 84 ) 85 ); 86 } 87 88 if ($this->showNewContenttype) { 89 $newKeywords = $keywords; 90 $newKeywords[] = $newContenttype; 91 $newKeywords[] = 'New ' . $singularname; 92 93 $this->register( 94 array( 95 'keywords' => $newKeywords, 96 'label' => $newContenttype, 97 'description' => '', 98 'priority' => self::OMNISEARCH_CONTENTTYPE, 99 'path' => $this->app->generatePath('editcontent', array('contenttypeslug' => $slug)), 100 ) 101 ); 102 } 103 } 104 } 105 106 private function initMenuitems() 107 { 108 // Configuration 109 if ($this->showConfiguration) { 110 $this->register( 111 array( 112 'keywords' => array('Configuration'), 113 'label' => Trans::__('Configuration'), 114 'description' => '', 115 'priority' => self::OMNISEARCH_MENUITEM, 116 'path' => $this->app->generatePath('fileedit', array('namespace' => 'config', 'file' => 'config.yml')), 117 ) 118 ); 119 $this->register( 120 array( 121 'keywords' => array('Users', 'Configuration'), 122 'label' => Trans::__('Configuration') . ' » ' . Trans::__('Users'), 123 'description' => '', 124 'priority' => self::OMNISEARCH_MENUITEM - 1, 125 'path' => $this->app->generatePath('users'), 126 ) 127 ); 128 $this->register( 129 array( 130 'keywords' => array('Contenttypes', 'Configuration'), 131 'label' => Trans::__('Configuration') . ' » ' . Trans::__('Contenttypes'), 132 'description' => '', 133 'priority' => self::OMNISEARCH_MENUITEM - 2, 134 'path' => $this->app->generatePath('fileedit', array('namespace' => 'config', 'file' => 'contenttypes.yml')), 135 ) 136 ); 137 $this->register( 138 array( 139 'keywords' => array('Taxonomy', 'Configuration'), 140 'label' => Trans::__('Configuration') . ' » ' . Trans::__('Taxonomy'), 141 'description' => '', 142 'priority' => self::OMNISEARCH_MENUITEM - 3, 143 'path' => $this->app->generatePath('fileedit', array('namespace' => 'config', 'file' => 'taxonomy.yml')), 144 ) 145 ); 146 $this->register( 147 array( 148 'keywords' => array('Menu setup', 'Configuration'), 149 'label' => Trans::__('Configuration') . ' » ' . Trans::__('Menu setup'), 150 'description' => '', 151 'priority' => self::OMNISEARCH_MENUITEM - 4, 152 'path' => $this->app->generatePath('fileedit', array('namespace' => 'config', 'file' => 'menu.yml')), 153 ) 154 ); 155 $this->register( 156 array( 157 'keywords' => array('Routing setup', 'Configuration'), 158 'label' => Trans::__('Configuration') . ' » ' . Trans::__('menu.configuration.routing'), 159 'description' => '', 160 'priority' => self::OMNISEARCH_MENUITEM - 5, 161 'path' => $this->app->generatePath('fileedit', array('namespace' => 'config', 'file' => 'routing.yml')), 162 ) 163 ); 164 } 165 166 // Maintenance 167 if ($this->showMaintenance) { 168 $this->register( 169 array( 170 'keywords' => array('Extensions', 'Maintenance'), 171 'label' => Trans::__('Maintenance') . ' » ' . Trans::__('Extensions'), 172 'description' => '', 173 'priority' => self::OMNISEARCH_MENUITEM - 6, 174 'path' => $this->app->generatePath('extend'), 175 ) 176 ); 177 $this->register( 178 array( 179 'keywords' => array('Check database', 'Maintenance'), 180 'label' => Trans::__('Maintenance') . ' » ' . Trans::__('Check database'), 181 'description' => '', 182 'priority' => self::OMNISEARCH_MENUITEM - 7, 183 'path' => $this->app->generatePath('dbcheck'), 184 ) 185 ); 186 $this->register( 187 array( 188 'keywords' => array('Clear the cache', 'Maintenance'), 189 'label' => Trans::__('Maintenance') . ' » ' . Trans::__('Clear the cache'), 190 'description' => '', 191 'priority' => self::OMNISEARCH_MENUITEM - 8, 192 'path' => $this->app->generatePath('clearcache'), 193 ) 194 ); 195 $this->register( 196 array( 197 'keywords' => array('Change log', 'Maintenance'), 198 'label' => Trans::__('Maintenance') . ' » ' . Trans::__('logs.change-log'), 199 'description' => '', 200 'priority' => self::OMNISEARCH_MENUITEM - 9, 201 'path' => $this->app->generatePath('changelog'), 202 ) 203 ); 204 $this->register( 205 array( 206 'keywords' => array('System log', 'Maintenance'), 207 'label' => Trans::__('Maintenance') . ' » ' . Trans::__('logs.system-log'), 208 'description' => '', 209 'priority' => self::OMNISEARCH_MENUITEM - 10, 210 'path' => $this->app->generatePath('systemlog'), 211 ) 212 ); 213 } 214 } 215 216 private function initExtensions() 217 { 218 if (!$this->showExtensions) { 219 return; 220 } 221 222 $extensionsmenu = $this->app['extensions']->getMenuoptions(); 223 $index = 0; 224 foreach ($extensionsmenu as $extension) { 225 $this->register( 226 array( 227 'keywords' => array($extension['label'], 'Extensions'), 228 'label' => Trans::__('Extensions') . ' » ' . $extension['label'], 229 'description' => '', 230 'priority' => self::OMNISEARCH_EXTENSION - $index, 231 'path' => $extension['path'], 232 ) 233 ); 234 235 $index--; 236 } 237 } 238 239 public function register($options) 240 { 241 // options 242 // $options['label']; // label shown in the search results 243 // $options['description']; // currently unused 244 // $options['keywords']; // array with descriptions to match for 245 // $options['priority']; // higher number, higher priority 246 // $options['path']; // the URL to go to 247 248 // automatically adds the translations 249 $keywords = $options['keywords']; 250 foreach ($keywords as $keyword) { 251 $options['keywords'][] = Trans::__($keyword); 252 } 253 254 $this->data[$options['path']] = $options; 255 } 256 257 public function query($query, $withRecord = false) 258 { 259 $options = array(); 260 261 $this->find($query, 'theme', '*.twig', $query, -10); // find in file contents 262 $this->find($query, 'theme', '*' . $query . '*.twig', false, 10); // find in filenames 263 $this->search($query, $withRecord); 264 265 foreach ($this->data as $item) { 266 $matches = $this->matches($item['path'], $query); 267 268 if (!$matches) { 269 foreach ($item['keywords'] as $keyword) { 270 if ($this->matches($keyword, $query)) { 271 $matches = true; 272 break; 273 } 274 } 275 } 276 277 if ($matches) { 278 $options[] = $item; 279 } 280 } 281 282 if ($this->showLandingpage) { 283 $options[] = array( 284 'keywords' => array('Omnisearch'), 285 'label' => sprintf("%s", Trans::__('Omnisearch')), 286 'description' => '', 287 'priority' => self::OMNISEARCH_LANDINGPAGE, 288 'path' => $this->app->generatePath('omnisearch', array('q' => $query)), 289 ); 290 } 291 292 usort($options, array($this, 'compareOptions')); 293 294 return $options; 295 } 296 297 /** 298 * Find in files. 299 * 300 * @param string $query 301 * @param string $path 302 * @param string $name 303 * @param bool|string $contains 304 * @param int $priority 305 */ 306 private function find($query, $path = 'theme', $name = '*.twig', $contains = false, $priority = 0) 307 { 308 if (!$this->showFiles) { 309 return; 310 } 311 312 $finder = new Finder(); 313 $finder->files() 314 ->ignoreVCS(true) 315 ->notName('*~') 316 ->in($this->app['resources']->getPath($path)); 317 318 if ($name) { 319 $finder->name($name); 320 } 321 322 if ($contains) { 323 $finder->contains($contains); 324 } 325 326 /** @var \Symfony\Component\Finder\SplFileInfo $file */ 327 foreach ($finder as $file) { 328 $relativePathname = $file->getRelativePathname(); 329 $filename = $file->getFilename(); 330 331 $this->register( 332 array( 333 'label' => sprintf("%s » <span>%s</span>", Trans::__('Edit file'), $filename), 334 'path' => $this->app->generatePath('fileedit', array('namespace' => 'theme', 'file' => $relativePathname)), 335 'description' => '', 336 'priority' => self::OMNISEARCH_FILE + $priority, 337 'keywords' => array('Edit file', $filename, $query) 338 ) 339 ); 340 } 341 } 342 343 /** 344 * Search in database. 345 * 346 * @param string $query 347 * @param bool $withRecord 348 */ 349 private function search($query, $withRecord = false) 350 { 351 if (!$this->showRecords) { 352 return; 353 } 354 $user = $this->app['users']->getCurrentUser(); 355 356 $searchresults = $this->app['storage']->searchContent($query); 357 /** @var Content[] $searchresults */ 358 $searchresults = $searchresults['results']; 359 360 $index = 0; 361 foreach ($searchresults as $result) { 362 $item = array( 363 'label' => sprintf( 364 '%s %s № %s » <span>%s</span>', 365 Trans::__('Edit'), 366 $result->contenttype['singular_name'], 367 $result->id, 368 $result->getTitle() 369 ), 370 'path' => $result->editlink(), 371 'description' => '', 372 'keywords' => array($query), 373 'priority' => self::OMNISEARCH_CONTENT - $index++, 374 ); 375 376 if ($withRecord) { 377 $item['record'] = $result; 378 $item['permissions'] = $this->app['permissions']->getContentTypeUserPermissions($result->contenttype['slug'], $user); 379 } 380 381 $this->register($item); 382 } 383 } 384 385 private function matches($sentence, $word) 386 { 387 return stripos($sentence, $word) !== false; 388 } 389 390 /** 391 * OmnisearchOption implements Comparable. 392 * 393 * @param array $a 394 * @param array $b 395 * 396 * @return int 397 */ 398 private function compareOptions($a, $b) 399 { 400 $comparison = $b['priority'] - $a['priority']; 401 402 if ($comparison == 0) { 403 return strcasecmp($a['path'], $b['path']); 404 } 405 406 return $comparison; 407 } 408} 409