1<?php
2
3namespace Bolt;
4
5use Bolt\Exception\LowlevelException;
6use Bolt\Helpers\Str;
7use Bolt\Library as Lib;
8use Bolt\Provider\LoggerServiceProvider;
9use Bolt\Provider\PathServiceProvider;
10use Bolt\Translation\Translator as Trans;
11use Cocur\Slugify\Bridge\Silex\SlugifyServiceProvider;
12use Doctrine\DBAL\DBALException;
13use RandomLib;
14use SecurityLib;
15use Silex;
16use Symfony\Component\HttpFoundation\Request;
17use Symfony\Component\HttpFoundation\Response;
18use Symfony\Component\HttpKernel\Exception\HttpException;
19use Symfony\Component\Stopwatch;
20use Whoops\Handler\JsonResponseHandler;
21use Whoops\Provider\Silex\WhoopsServiceProvider;
22
23class Application extends Silex\Application
24{
25    /**
26     * The default locale, used as fallback.
27     */
28    const DEFAULT_LOCALE = 'en_GB';
29
30    /**
31     * @param array $values
32     */
33    public function __construct(array $values = array())
34    {
35        $values['bolt_version'] = '2.2.24';
36        $values['bolt_name'] = '';
37        $values['bolt_released'] = true; // `true` for stable releases, `false` for alpha, beta and RC.
38
39        /** @internal Parameter to track a deprecated PHP version */
40        $values['deprecated.php'] = version_compare(PHP_VERSION, '5.4.0', '<');
41
42        parent::__construct($values);
43
44        $this->register(new PathServiceProvider());
45
46        // Initialize the config. Note that we do this here, on 'construct'.
47        // All other initialization is triggered from bootstrap.php
48        // Warning!
49        // One of a valid ResourceManager ['resources'] or ClassLoader ['classloader']
50        // must be defined for working properly
51        if (!isset($this['resources'])) {
52            $this['resources'] = new Configuration\ResourceManager($this);
53            $this['resources']->compat();
54        } else {
55            $this['classloader'] = $this['resources']->getClassLoader();
56        }
57
58        $this['resources']->setApp($this);
59        $this->initConfig();
60        $this->initSession();
61        $this['resources']->initialize();
62
63        $this['debug'] = $this['config']->get('general/debug', false);
64        $this['debugbar'] = false;
65
66        // Initialize the 'editlink' and 'edittitle'.
67        $this['editlink'] = '';
68        $this['edittitle'] = '';
69
70        // Initialize the JavaScript data gateway
71        $this['jsdata'] = array();
72    }
73
74    protected function initConfig()
75    {
76        $this->register(new Provider\IntegrityCheckerProvider())
77            ->register(new Provider\ConfigServiceProvider());
78    }
79
80    protected function initSession()
81    {
82        $this->register(
83            new Silex\Provider\SessionServiceProvider(),
84            array(
85                'session.storage.options' => array(
86                    'name'            => 'bolt_session',
87                    'cookie_secure'   => $this['config']->get('general/enforce_ssl'),
88                    'cookie_httponly' => true
89                )
90            )
91        );
92
93        // Disable Silex's built-in native filebased session handler, and fall back to
94        // whatever's set in php.ini.
95        // @see: http://silex.sensiolabs.org/doc/providers/session.html#custom-session-configurations
96        if ($this['config']->get('general/session_use_storage_handler') === false) {
97            $this['session.storage.handler'] = null;
98        }
99    }
100
101    public function initialize()
102    {
103        // Initialize logging.
104        $this->initLogger();
105
106        // Set up locale and translations.
107        $this->initLocale();
108
109        // Initialize Twig and our rendering Provider.
110        $this->initRendering();
111
112        // Initialize Web Profiler Providers if enabled.
113        $this->initProfiler();
114
115        // Initialize the Database Providers.
116        $this->initDatabase();
117
118        // Initialize the rest of the Providers.
119        $this->initProviders();
120
121        // Initialise the Mount points for 'frontend', 'backend' and 'async'.
122        $this->initMountpoints();
123
124        // Initialize enabled extensions before executing handlers.
125        $this->initExtensions();
126
127        // Mail config checks for extensions
128        $this->before(array($this, 'initMailCheck'));
129
130        // Initialize the global 'before' handler.
131        $this->before(array($this, 'beforeHandler'));
132
133        // Initialize the global 'after' handler.
134        $this->after(array($this, 'afterHandler'));
135
136        // Initialize the 'error' handler.
137        $this->error(array($this, 'errorHandler'));
138    }
139
140    /**
141     * Initialize the loggers.
142     */
143    public function initLogger()
144    {
145        $this->register(new LoggerServiceProvider(), array());
146
147        // Debug log
148        if ($this['config']->get('general/debuglog/enabled')) {
149            $this->register(
150                new Silex\Provider\MonologServiceProvider(),
151                array(
152                    'monolog.name'    => 'bolt',
153                    'monolog.level'   => constant('Monolog\Logger::' . strtoupper($this['config']->get('general/debuglog/level'))),
154                    'monolog.logfile' => $this['resources']->getPath('cache') . '/' . $this['config']->get('general/debuglog/filename')
155                )
156            );
157        }
158    }
159
160    /**
161     * Initialize the database providers.
162     */
163    public function initDatabase()
164    {
165        $this->register(
166            new Silex\Provider\DoctrineServiceProvider(),
167            array(
168                'db.options' => $this['config']->get('general/database')
169            )
170        );
171        $this->register(new Database\InitListener());
172
173        // Filter tables to only those bolt cares about (based on table name prefix).
174        // This prevents schema parsing errors from tables we don't need to worry about.
175        $app = $this;
176        // This key will make more since in version 2.3
177        $this['schema.tables_filter'] = function () use ($app) {
178            $prefix = $app['config']->get('general/database/prefix');
179            return "/^$prefix.+/";
180        };
181        $this['db.config'] = $this->share(
182            $this->extend('db.config',
183                function ($config) use ($app) {
184                    $config->setFilterSchemaAssetsExpression($app['schema.tables_filter']);
185
186                    return $config;
187                }
188            )
189        );
190
191        $this->checkDatabaseConnection();
192
193        $this->register(
194            new Silex\Provider\HttpCacheServiceProvider(),
195            array('http_cache.cache_dir' => $this['resources']->getPath('cache'))
196        );
197    }
198
199    /**
200     * Set up the DBAL connection now to check for a proper connection to the database.
201     *
202     * @throws LowlevelException
203     */
204    protected function checkDatabaseConnection()
205    {
206        // [SECURITY]: If we get an error trying to connect to database, we throw a new
207        // LowLevelException with general information to avoid leaking connection information.
208        try {
209            $this['db']->connect();
210        // A ConnectionException or DriverException could be thrown, we'll catch DBALException to be safe.
211        } catch (DBALException $e) {
212            // Trap double exceptions caused by throwing a new LowlevelException
213            set_exception_handler(array('\Bolt\Exception\LowlevelException', 'nullHandler'));
214
215            /*
216             * Using Driver here since Platform may try to connect
217             * to the database, which has failed since we are here.
218             */
219            $platform = $this['db']->getDriver()->getName();
220            $platform = Str::replaceFirst('pdo_', '', $platform);
221
222            $error = "Bolt could not connect to the configured database.\n\n" .
223                     "Things to check:\n" .
224                     "&nbsp;&nbsp;* Ensure the $platform database is running\n" .
225                     "&nbsp;&nbsp;* Check the <code>database:</code> parameters are configured correctly in <code>app/config/config.yml</code>\n" .
226                     "&nbsp;&nbsp;&nbsp;&nbsp;* Database name is correct\n" .
227                     "&nbsp;&nbsp;&nbsp;&nbsp;* User name has access to the named database\n" .
228                     "&nbsp;&nbsp;&nbsp;&nbsp;* Password is correct\n";
229            throw new LowlevelException($error);
230        }
231
232        // Resume normal error handling
233        restore_error_handler();
234    }
235
236    /**
237     * Initialize the rendering providers.
238     */
239    public function initRendering()
240    {
241        $this->register(new Provider\TwigServiceProvider());
242        $this->register(new Provider\SafeTwigServiceProvider());
243
244        $this->register(new Provider\RenderServiceProvider());
245        $this->register(new Provider\RenderServiceProvider(true));
246    }
247
248    /**
249     * Set up the profilers for the toolbar.
250     */
251    public function initProfiler()
252    {
253        // On 'after' attach the debug-bar, if debug is enabled.
254        if (!($this['debug'] && ($this['session']->has('user') || $this['config']->get('general/debug_show_loggedoff')))) {
255            error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_USER_DEPRECATED);
256
257            return;
258        }
259
260        // Set the error_reporting to the level specified in config.yml
261        error_reporting($this['config']->get('general/debug_error_level'));
262
263        // Register Whoops, to handle errors for logged in users only.
264        if ($this['config']->get('general/debug_enable_whoops')) {
265            $this->register(new WhoopsServiceProvider());
266
267            // Add a special handler to deal with AJAX requests
268            if ($this['config']->getWhichEnd() == 'async') {
269                $this['whoops']->pushHandler(new JsonResponseHandler());
270            }
271        }
272
273        // Register the Silex/Symfony web debug toolbar.
274        $this->register(
275            new Silex\Provider\WebProfilerServiceProvider(),
276            array(
277                'profiler.cache_dir'    => $this['resources']->getPath('cache') . '/profiler',
278                'profiler.mount_prefix' => '/_profiler', // this is the default
279            )
280        );
281
282        // Register the toolbar item for our Database query log.
283        $this->register(new Provider\DatabaseProfilerServiceProvider());
284
285        // Register the toolbar item for our bolt nipple.
286        $this->register(new Provider\BoltProfilerServiceProvider());
287
288        // Register the toolbar item for the Twig toolbar item.
289        $this->register(new Provider\TwigProfilerServiceProvider());
290
291        $this['twig.loader.filesystem'] = $this->share(
292            $this->extend(
293                'twig.loader.filesystem',
294                function (\Twig_Loader_Filesystem $filesystem, Application $app) {
295                    $refProfilerClass = new \ReflectionClass('Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension');
296                    $webProfilerPath = dirname(dirname($refProfilerClass->getFileName()));
297                    $filesystem->addPath(
298                        $webProfilerPath . '/Resources/views',
299                        'WebProfiler'
300                    );
301                    $filesystem->addPath($app['resources']->getPath('app') . '/view', 'BoltProfiler');
302
303                    return $filesystem;
304                }
305            )
306        );
307
308        // PHP 5.3 does not allow 'use ($this)' in closures.
309        $app = $this;
310        $this->after(
311            function () use ($app) {
312                foreach (Lib::parseTwigTemplates($app['twig.loader.filesystem']) as $template) {
313                    $app['twig.logger']->collectTemplateData($template);
314                }
315            }
316        );
317    }
318
319    public function initLocale()
320    {
321        $configLocale = $this['config']->get('general/locale', Application::DEFAULT_LOCALE);
322        if (!is_array($configLocale)) {
323            $configLocale = array($configLocale);
324        }
325
326        // $app['locale'] should only be a single value.
327        $this['locale'] = reset($configLocale);
328
329        // Set the default timezone if provided in the Config
330        date_default_timezone_set($this['config']->get('general/timezone') ?: ini_get('date.timezone') ?: 'UTC');
331
332        // for javascript datetime calculations, timezone offset. e.g. "+02:00"
333        $this['timezone_offset'] = date('P');
334
335        // Set default locale, for Bolt
336        $locale = array();
337        foreach ($configLocale as $value) {
338            $locale = array_merge($locale, array(
339                $value . '.UTF-8',
340                $value . '.utf8',
341                $value,
342                Application::DEFAULT_LOCALE . '.UTF-8',
343                Application::DEFAULT_LOCALE . '.utf8',
344                Application::DEFAULT_LOCALE,
345                substr(Application::DEFAULT_LOCALE, 0, 2)
346            ));
347        }
348
349        setlocale(LC_ALL, array_unique($locale));
350
351        $this->register(
352            new Silex\Provider\TranslationServiceProvider(),
353            array(
354                'translator.cache_dir' => $this['resources']->getPath('cache/trans'),
355                'locale_fallbacks'     => array(Application::DEFAULT_LOCALE)
356                )
357        );
358
359        // Loading stub functions for when intl / IntlDateFormatter isn't available.
360        if (!function_exists('intl_get_error_code')) {
361            require_once $this['resources']->getPath('root/vendor/symfony/locale/Symfony/Component/Locale/Resources/stubs/functions.php');
362            require_once $this['resources']->getPath('root/vendor/symfony/locale/Symfony/Component/Locale/Resources/stubs/IntlDateFormatter.php');
363        }
364
365        $this->register(new Provider\TranslationServiceProvider());
366    }
367
368    public function initProviders()
369    {
370        // Make sure we keep our current locale.
371        $currentlocale = $this['locale'];
372
373        // Setup Swiftmailer, with the selected Mail Transport options: smtp or `mail()`.
374        $this->register(new Silex\Provider\SwiftmailerServiceProvider());
375
376        if ($this['config']->get('general/mailoptions')) {
377            // Use the preferred options. Assume it's SMTP, unless set differently.
378            $this['swiftmailer.options'] = $this['config']->get('general/mailoptions');
379        }
380
381        if (is_bool($this['config']->get('general/mailoptions/spool'))) {
382            // enable or disable the mail spooler.
383            $this['swiftmailer.use_spool'] = $this['config']->get('general/mailoptions/spool');
384        }
385
386        if ($this['config']->get('general/mailoptions/transport') == 'mail') {
387            // Use the 'mail' transport. Discouraged, but some people want it. ¯\_(ツ)_/¯
388            $this['swiftmailer.transport'] = \Swift_MailTransport::newInstance();
389        }
390
391        // Set up our secure random generator.
392        $factory = new RandomLib\Factory();
393        $this['randomgenerator'] = $factory->getGenerator(new SecurityLib\Strength(SecurityLib\Strength::MEDIUM));
394
395        $this
396            ->register(new Silex\Provider\HttpFragmentServiceProvider())
397            ->register(new Silex\Provider\UrlGeneratorServiceProvider())
398            ->register(new Silex\Provider\FormServiceProvider())
399            ->register(new Silex\Provider\ValidatorServiceProvider())
400            ->register(new Provider\RoutingServiceProvider())
401            ->register(new Silex\Provider\ServiceControllerServiceProvider()) // must be after Routing
402            ->register(new Provider\PermissionsServiceProvider())
403            ->register(new Provider\StorageServiceProvider())
404            ->register(new Provider\UsersServiceProvider())
405            ->register(new Provider\CacheServiceProvider())
406            ->register(new Provider\ExtensionServiceProvider())
407            ->register(new Provider\StackServiceProvider())
408            ->register(new Provider\OmnisearchServiceProvider())
409            ->register(new Provider\TemplateChooserServiceProvider())
410            ->register(new Provider\CronServiceProvider())
411            ->register(new Provider\FilePermissionsServiceProvider())
412            ->register(new Provider\MenuServiceProvider())
413            ->register(new Controllers\Upload())
414            ->register(new Controllers\Extend())
415            ->register(new Provider\FilesystemProvider())
416            ->register(new Thumbs\ThumbnailProvider())
417            ->register(new Provider\NutServiceProvider())
418            ->register(new Provider\GuzzleServiceProvider())
419            ->register(new Provider\PrefillServiceProvider())
420            ->register(new SlugifyServiceProvider())
421            ->register(new Provider\MarkdownServiceProvider());
422
423        $this['paths'] = $this['resources']->getPaths();
424
425        // For some obscure reason, and under suspicious circumstances $app['locale'] might become 'null'.
426        // Re-set it here, just to be sure. See https://github.com/bolt/bolt/issues/1405
427        $this['locale'] = $currentlocale;
428
429        // Initialize stopwatch even if debug is not enabled.
430        $this['stopwatch'] = $this->share(
431            function () {
432                return new Stopwatch\Stopwatch();
433            }
434        );
435    }
436
437    public function initExtensions()
438    {
439        $this['extensions']->checkLocalAutoloader();
440        $this['extensions']->initialize();
441    }
442
443    /**
444     * No Mail transport has been set. We should gently nudge the user to set the mail configuration.
445     *
446     * @see: the issue at https://github.com/bolt/bolt/issues/2908
447     *
448     * For now, we only pester the user, if an extension needs to be able to send
449     * mail, but it's not been set up.
450     */
451    public function initMailCheck()
452    {
453        if ($this['users']->getCurrentuser() && !$this['config']->get('general/mailoptions') && $this['extensions']->hasMailSenders()) {
454            $error = "One or more installed extensions need to be able to send email. Please set up the 'mailoptions' in config.yml.";
455            $this['session']->getFlashBag()->add('error', Trans::__($error));
456        }
457    }
458
459    public function initMountpoints()
460    {
461        if ($proxies = $this['config']->get('general/trustProxies')) {
462            Request::setTrustedProxies($proxies);
463        }
464
465        // Mount the 'backend' on the branding:path setting. Defaults to '/bolt'.
466        $backendPrefix = $this['config']->get('general/branding/path');
467        $this->mount($backendPrefix, new Controllers\Login());
468        $this->mount($backendPrefix, new Controllers\Backend());
469
470        // Mount the 'async' controllers on /async. Not configurable.
471        $this->mount('/async', new Controllers\Async());
472
473        // Mount the 'thumbnail' provider on /thumbs.
474        $this->mount('/thumbs', new Thumbs\ThumbnailProvider());
475
476        // Mount the 'upload' controller on /upload.
477        $this->mount('/upload', new Controllers\Upload());
478
479        // Mount the 'extend' controller on /branding/extend.
480        $this->mount($backendPrefix . '/extend', $this['extend']);
481
482        if ($this['config']->get('general/enforce_ssl')) {
483            foreach ($this['routes'] as $route) {
484                /** @var \Silex\Route $route */
485                $route->requireHttps();
486            }
487        }
488
489        // Mount the 'frontend' controllers, as defined in our Routing.yml
490        $this->mount('', new Controllers\Routing());
491    }
492
493    public function beforeHandler(Request $request)
494    {
495        // Start the 'stopwatch' for the profiler.
496        $this['stopwatch']->start('bolt.app.before');
497
498        if ($response = $this['render']->fetchCachedRequest()) {
499            // Stop the 'stopwatch' for the profiler.
500            $this['stopwatch']->stop('bolt.app.before');
501
502            // Short-circuit the request, return the HTML/response. YOLO.
503            return $response;
504        }
505
506        // Stop the 'stopwatch' for the profiler.
507        $this['stopwatch']->stop('bolt.app.before');
508    }
509
510    /**
511     * Remove the 'bolt_session' cookie from the headers if it's about to be set.
512     *
513     * Note, we don't use $request->clearCookie (logs out a logged-on user) or
514     * $request->removeCookie (doesn't prevent the header from being sent).
515     *
516     * @see https://github.com/bolt/bolt/issues/3425
517     */
518    public function unsetSessionCookie()
519    {
520        if (!headers_sent()) {
521            $headersList = headers_list();
522            foreach ($headersList as $header) {
523                if (strpos($header, "Set-Cookie: bolt_session=") === 0) {
524                    header_remove("Set-Cookie");
525                }
526            }
527        }
528    }
529
530    /**
531     * Global 'after' handler. Adds 'after' HTML-snippets and Meta-headers to the output.
532     *
533     * @param Request  $request
534     * @param Response $response
535     */
536    public function afterHandler(Request $request, Response $response)
537    {
538        // Start the 'stopwatch' for the profiler.
539        $this['stopwatch']->start('bolt.app.after');
540
541        /*
542         * Don't set 'bolt_session' cookie, if we're in the frontend or async.
543         *
544         * @see https://github.com/bolt/bolt/issues/3425
545         */
546        if ($this['config']->get('general/cookies_no_frontend', false) && $this['config']->getWhichEnd() !== 'backend') {
547            $this->unsetSessionCookie();
548        }
549
550        // Set the 'X-Frame-Options' headers to prevent click-jacking, unless specifically disabled. Backend only!
551        if ($this['config']->getWhichEnd() == 'backend' && $this['config']->get('general/headers/x_frame_options')) {
552            $response->headers->set('X-Frame-Options', 'SAMEORIGIN');
553            $response->headers->set('Frame-Options', 'SAMEORIGIN');
554        }
555
556        // Exit now if it's an AJAX call
557        if ($request->isXmlHttpRequest()) {
558            $this['stopwatch']->stop('bolt.app.after');
559
560            return;
561        }
562
563        // true if we need to consider adding html snippets
564        // note we exclude cached requests where no additions should be made to the HTML
565        if (isset($this['htmlsnippets'])
566            && ($this['htmlsnippets'] === true)
567            && (in_array($response->headers->get('Cache-Control'), array('no-cache','private, must-revalidate')))
568        ) {
569            // only add when content-type is text/html
570            if (strpos($response->headers->get('Content-Type'), 'text/html') !== false) {
571                // Add our meta generator tag.
572                $this['extensions']->insertSnippet(Extensions\Snippets\Location::END_OF_HEAD, '<meta name="generator" content="Bolt">');
573
574                // Perhaps add a canonical link.
575
576                if ($this['config']->get('general/canonical')) {
577                    $snippet = sprintf(
578                        '<link rel="canonical" href="%s">',
579                        htmlspecialchars($this['resources']->getUrl('canonicalurl'), ENT_QUOTES)
580                    );
581                    $this['extensions']->insertSnippet(Extensions\Snippets\Location::END_OF_HEAD, $snippet);
582                }
583
584                // Perhaps add a favicon.
585                if ($this['config']->get('general/favicon')) {
586                    $snippet = sprintf(
587                        '<link rel="shortcut icon" href="%s%s%s">',
588                        htmlspecialchars($this['resources']->getUrl('hosturl'), ENT_QUOTES),
589                        htmlspecialchars($this['resources']->getUrl('theme'), ENT_QUOTES),
590                        htmlspecialchars($this['config']->get('general/favicon'), ENT_QUOTES)
591                    );
592                    $this['extensions']->insertSnippet(Extensions\Snippets\Location::END_OF_HEAD, $snippet);
593                }
594
595                // Do some post-processing.. Hooks, snippets.
596                $html = $this['render']->postProcess($response);
597
598                $response->setContent($html);
599            }
600        }
601
602        // Stop the 'stopwatch' for the profiler.
603        $this['stopwatch']->stop('bolt.app.after');
604    }
605
606    /**
607     * Handle errors thrown in the application. Set up whoops, if set in conf.
608     *
609     * @param \Exception $exception
610     *
611     * @return Response
612     */
613    public function errorHandler(\Exception $exception)
614    {
615        // If we are in maintenance mode and current user is not logged in, show maintenance notice.
616        // @see Controllers\Frontend::before()
617        if ($this['config']->get('general/maintenance_mode')) {
618            $user = $this['users']->getCurrentUser();
619            if ($user['userlevel'] < 2) {
620                $template = $this['config']->get('general/maintenance_template');
621                $body = $this['render']->render($template);
622
623                return new Response($body, Response::HTTP_SERVICE_UNAVAILABLE);
624            }
625        }
626
627        // Log the error message
628        $message = $exception->getMessage();
629        $this['logger.system']->critical($message, array('event' => 'exception', 'exception' => $exception));
630
631        $trace = $exception->getTrace();
632        foreach ($trace as $key => $value) {
633            if (!empty($value['file']) && strpos($value['file'], '/vendor/') > 0) {
634                unset($trace[$key]['args']);
635            }
636
637            // Don't display the full path.
638            if (isset($trace[$key]['file'])) {
639                $trace[$key]['file'] = str_replace($this['resources']->getPath('root'), '[root]', $trace[$key]['file']);
640            }
641        }
642
643        $end = $this['config']->getWhichEnd();
644        if (($exception instanceof HttpException) && ($end == 'frontend')) {
645            if (substr($this['config']->get('general/notfound'), -5) === '.twig') {
646                return $this['render']->render($this['config']->get('general/notfound'));
647            } else {
648                $content = $this['storage']->getContent($this['config']->get('general/notfound'), array('returnsingle' => true));
649
650                // Then, select which template to use, based on our 'cascading templates rules'
651                if ($content instanceof Content && !empty($content->id)) {
652                    $template = $this['templatechooser']->record($content);
653
654                    return $this['render']->render($template, $content->getTemplateContext());
655                }
656            }
657
658            $message = "The page could not be found, and there is no 'notfound' set in 'config.yml'. Sorry about that.";
659        }
660
661        $context = array(
662            'class'   => get_class($exception),
663            'message' => $message,
664            'code'    => $exception->getCode(),
665            'trace'   => $trace,
666        );
667
668        // Note: This uses the template from app/theme_defaults. Not app/view/twig.
669        return $this['render']->render('error.twig', array('context' => $context));
670    }
671
672    /**
673     * @todo Can this be removed?
674     *
675     * @param string $name
676     *
677     * @return boolean
678     */
679    public function __isset($name)
680    {
681        return isset($this[$name]);
682    }
683
684    /**
685     * Get the Bolt version string
686     *
687     * @param boolean $long TRUE returns 'version name', FALSE 'version'
688     *
689     * @return string
690     */
691    public function getVersion($long = true)
692    {
693        if ($long) {
694            return trim($this['bolt_version'] . ' ' . $this['bolt_name']);
695        }
696
697        return $this['bolt_version'];
698    }
699
700    /**
701     * Generates a path from the given parameters.
702     *
703     * Note: This can be pulled in from Silex\Application\UrlGeneratorTrait
704     * once we support Traits.
705     *
706     * @param string $route      The name of the route
707     * @param array  $parameters An array of parameters
708     *
709     * @return string The generated path
710     */
711    public function generatePath($route, $parameters = array())
712    {
713        return $this['url_generator']->generate($route, $parameters);
714    }
715}
716