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