<?php
/**
* Perforce Swarm
*
* @copyright 2012 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace Application;
use Application\Permissions\Csrf\Listener as CsrfListener;
use Application\Response\CallbackResponseSender;
use Application\View\Http\ExceptionStrategy;
use Application\View\Http\RouteNotFoundStrategy;
use Application\View\Http\StrictJsonStrategy;
use Zend\Console\Request as ConsoleRequest;
use Zend\Http\Response as HttpResponse;
use Zend\Http\Request as HttpRequest;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\ResponseSender\SendResponseEvent;
use Zend\Mvc\Router\Http\RouteMatch;
use Zend\ServiceManager\ServiceLocatorInterface as ServiceLocator;
use Zend\Mvc\View\Console\RouteNotFoundStrategy as ConsoleRouteNotFoundStrategy;
use Zend\Stdlib\Parameters;
use Zend\Validator\AbstractValidator;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$services = $application->getServiceManager();
$config = $services->get('config');
$events = $application->getEventManager();
$services->setAllowOverride(true);
// attach our custom exception strategy to control http result codes
$exceptionStrategy = new ExceptionStrategy;
$exceptionStrategy->attach($events);
// attach our JSON strategy to strictly enforce JSON output when requested
$strictJsonStrategy = new StrictJsonStrategy;
$strictJsonStrategy->attach($events);
// remove Zend's route not found strategy and attach our own
// ours does not muck with JSON responses (we think that is bad)
$this->replaceNotFoundStrategy($event);
// attempt to select a UTF-8 compatible locale, this is important
// to avoid corrupting unicode characters when manipulating strings
$this->setUtf8Locale($services);
// enable localized validator messages
$translator = $services->get('translator');
AbstractValidator::setDefaultTranslator($translator);
// only allow same origin framing to deter clickjacking
if ($application->getResponse() instanceof HttpResponse) {
$application->getResponse()->getHeaders()->addHeaderLine('X-Frame-Options: SAMEORIGIN');
}
// if strict https is set, tell browsers to only use SSL for the next 30 days
// if strict https is set, along with redirect, and we're on http add a meta-refresh to goto https
if ($application->getResponse() instanceof HttpResponse
&& isset($config['security']['https_strict']) && $config['security']['https_strict']
) {
// always add the HSTS header, HTTP Clients will just ignore it
$application->getResponse()->getHeaders()->addHeaderLine('Strict-Transport-Security: max-age=2592000');
// if we came in on http and redirection is enabled, add a meta-refresh
$uri = $application->getRequest()->getUri();
if ($uri->getScheme() == 'http'
&& isset($config['security']['https_strict_redirect']) && $config['security']['https_strict_redirect']
) {
$port = isset($config['security']['https_port']) ? $config['security']['https_port'] : null;
$uri = clone $uri;
$uri->setScheme('https');
$uri->setPort($port);
$services->get('ViewHelperManager')->get('HeadMeta')->appendHttpEquiv('Refresh', '0;URL=' . $uri);
}
}
// ensure a timezone was set to quell later warnings
date_default_timezone_set(@date_default_timezone_get());
// enable the display of errors in development mode.
$dev = isset($config['environment']['mode']) && $config['environment']['mode'] == 'development';
ini_set('display_errors', $dev ? 1 : 0);
ini_set('display_startup_errors', $dev ? 1 : 0);
if ($dev) {
$services->get('viewmanager')
->getExceptionStrategy()
->setDisplayExceptions(true);
}
// attach callback response sender, making it the penultimate listener (to avoid hitting the catch-all)
// default listeners are registered in Zend\Mvc\SendResponseListener::attachDefaultListeners()
$responseListener = $services->get('SendResponseListener')->getEventManager();
$responseListener->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new CallbackResponseSender(), -3500);
// log exceptions.
$events->attach(
array(MvcEvent::EVENT_DISPATCH_ERROR, MvcEvent::EVENT_RENDER_ERROR),
function ($event) use ($services) {
if ($event->getParam('exception')) {
$services->get('logger')->crit($event->getParam('exception'));
}
}
);
// require login to view swarm if the require_login parameter is set to true
if (isset($config['security']['require_login']) && $config['security']['require_login']) {
$events->attach(
MvcEvent::EVENT_ROUTE,
function ($event) use ($services) {
$config = $services->get('config');
$routeMatch = $event->getRouteMatch();
$config['security'] += array('login_exempt' => array());
$exemptRoutes = (array) $config['security']['login_exempt'];
// continue if its a console request (not HTTP)
if ($event->getRequest() instanceof \Zend\Console\Request) {
return;
}
// continue if route is login exempt
if (in_array($routeMatch->getMatchedRouteName(), $exemptRoutes)) {
return;
}
// forward to login method if the user isn't logged in
if (!$services->get('permissions')->is('authenticated')) {
$routeMatch = new RouteMatch(
array(
'controller' => 'Users\Controller\Index',
'action' => 'login'
)
);
// clear out the post and query parameters, preserving the "format" if it specifies JSON
$query = new Parameters;
$post = new Parameters;
if (strtolower($event->getRequest()->getQuery('format')) === 'json') {
$query->set('format', 'json');
}
$routeMatch->setMatchedRouteName('login');
$event->setRouteMatch($routeMatch);
$event->getRequest()->setPost($post)->setQuery($query);
$event->getResponse()->setStatusCode(401);
}
},
-1000 // execute last after the route has been determined
);
}
// enforce CSRF protection
// if this isn't a get request, isn't an exempt route and isn't an anonymous user; you best be token'ed
$csrfListener = new CsrfListener($services);
$csrfListener->attach($events);
// normalize the hostname if one is set.
// users might erroneously include a scheme or port when all we want is a host.
if (!empty($config['environment']['hostname'])) {
preg_match('#^([a-z]+://)?(?P<hostname>[^:]+)?#', $config['environment']['hostname'], $matches);
$config['environment']['hostname'] = isset($matches['hostname']) ? $matches['hostname'] : null;
$services->setService('config', $config);
}
// derive the hostname from the request if one isn't set.
if (empty($config['environment']['hostname']) && $application->getRequest() instanceof HttpRequest) {
$config['environment']['hostname'] = $application->getRequest()->getUri()->getHost();
$services->setService('config', $config);
}
// ensure the various view helpers use our escaper as it
// will replace invalid utf-8 byte sequences with an inverted
// question mark, zend's version would simply blow up.
$escaper = new Escaper\Escaper;
$helpers = $services->get('ViewHelperManager');
$helpers->get('escapeCss')->setEscaper($escaper);
$helpers->get('escapeHtml')->setEscaper($escaper);
$helpers->get('escapeHtmlAttr')->setEscaper($escaper);
$helpers->get('escapeJs')->setEscaper($escaper);
$helpers->get('escapeUrl')->setEscaper($escaper);
$helpers->get('escapeFullUrl')->setEscaper($escaper);
// define the version constants
$file = BASE_PATH . '/Version';
$values = file_exists($file) ? parse_ini_file($file) : array();
$values += array('RELEASE' => 'unknown', 'PATCHLEVEL' => 'unknown', 'SUPPDATE' => date('Y/m/d'));
if (!defined('VERSION_NAME')) {
define('VERSION_NAME', 'SWARM');
}
if (!defined('VERSION_RELEASE')) {
define('VERSION_RELEASE', strtr(preg_replace('/ /', '.', $values['RELEASE'], 1), ' ', '-'));
}
if (!defined('VERSION_PATCHLEVEL')) {
define('VERSION_PATCHLEVEL', $values['PATCHLEVEL']);
}
if (!defined('VERSION_SUPPDATE')) {
define('VERSION_SUPPDATE', strtr($values['SUPPDATE'], ' ', '/'));
}
if (!defined('VERSION')) {
define(
'VERSION',
VERSION_NAME . '/' . VERSION_RELEASE . '/' . VERSION_PATCHLEVEL . ' (' . VERSION_SUPPDATE . ')'
);
}
// attach cache-clearing task to worker #1 shutdown
$services->get('queue')->getEventManager()->attach(
'worker.shutdown',
function ($event) use ($services) {
// only run for the first worker
if ($event->getParam('slot') !== 1) {
return;
}
try {
$p4Admin = $services->get('p4_admin');
$cache = $p4Admin->getService('cache');
$cache->removeInvalidatedFiles();
} catch (Exception $e) {
$services->get('logger')->err($e);
}
}
);
// set base url on the request
// we take base url from the config if set, otherwise from the server (defaults to empty string)
$request = $application->getRequest();
if (!$request instanceof ConsoleRequest) {
$baseUrl = isset($config['environment']['baseurl'])
? $config['environment']['baseurl']
: $request->getServer()->get('REQUEST_BASE_URL', '');
$request->setBaseUrl($baseUrl);
}
}
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
/**
* Remove Zend's route not found strategy and attach our own
* ours does not muck with JSON responses (we think that is bad)
*
* @param MvcEvent $event
*/
public function replaceNotFoundStrategy(MvcEvent $event)
{
$application = $event->getApplication();
$services = $application->getServiceManager();
$events = $application->getEventManager();
$notFoundStrategy = $services->get('viewmanager')->getRouteNotFoundStrategy();
$sharedEvents = $events->getSharedManager();
$sharedListeners = $sharedEvents->getListeners('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH);
// console routes don't need this treatment
if ($notFoundStrategy instanceof ConsoleRouteNotFoundStrategy) {
return;
}
// detach from the general event manager
$notFoundStrategy->detach($events);
// detach from the shared events manager
foreach ($sharedListeners as $listener) {
if (current($listener->getCallback()) === $notFoundStrategy) {
$sharedEvents->detach('Zend\Stdlib\DispatchableInterface', $listener);
}
}
$oldNotFoundStrategy = $notFoundStrategy;
$notFoundStrategy = new RouteNotFoundStrategy;
// preserve behaviour from old strategy instance
$notFoundStrategy->setDisplayExceptions($oldNotFoundStrategy->displayExceptions());
$notFoundStrategy->setDisplayNotFoundReason($oldNotFoundStrategy->displayNotFoundReason());
$notFoundStrategy->setNotFoundTemplate($oldNotFoundStrategy->getNotFoundTemplate());
// update the stored service and attach the strategy to the event manager
$services->setService('RouteNotFoundStrategy', $notFoundStrategy);
$notFoundStrategy->attach($events);
}
/**
* Set the locale to one that supports UTF-8.
*
* Note: we only change the locale for LC_CTYPE as we only
* want to affect the behavior of string manipulation.
*
* @param ServiceLocator $services the service locator for logging purposes
*/
protected function setUtf8Locale(ServiceLocator $services)
{
$logger = $services->get('logger');
$pattern = '/\.utf\-?8$/i';
// if we are already using a utf8 locale, nothing to do.
if (preg_match($pattern, setlocale(LC_CTYPE, 0))) {
return;
}
// we don't want to run 'locale -a' for every request - cache it for 1hr.
$cacheFile = DATA_PATH . '/cache/system-locales';
\Record\Cache\Cache::ensureWritable(dirname($cacheFile));
if (file_exists($cacheFile) && (time() - (int) filemtime($cacheFile)) < 3600) {
$fromCache = true;
$locales = unserialize(file_get_contents($cacheFile));
} else {
$fromCache = false;
exec('locale -a', $locales, $result);
if ($result) {
$logger->err("Failed to exec 'locale -a'. Exit status: $result.");
$locales = array();
}
file_put_contents($cacheFile, serialize($locales));
}
foreach ($locales as $locale) {
if (preg_match($pattern, $locale) && setlocale(LC_CTYPE, $locale) !== false) {
return;
}
}
// we don't want to complain for every request - only report errors every 1hr.
if (!$fromCache) {
$logger->err("Failed to set a UTF-8 compatible locale.");
}
}
}