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 " * Ensure the $platform database is running\n" . 225 " * Check the <code>database:</code> parameters are configured correctly in <code>app/config/config.yml</code>\n" . 226 " * Database name is correct\n" . 227 " * User name has access to the named database\n" . 228 " * 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