Module.php #1

  • //
  • guest/
  • thomas_gray/
  • jambox/
  • main/
  • swarm/
  • module/
  • Application/
  • Module.php
  • View
  • Commits
  • Open Download .zip Download (15 KB)
<?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.");
        }
    }
}
# Change User Description Committed
#1 18334 Liz Lam initial add of jambox