<?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>
*/
return array(
'environment' => array(
'mode' => getenv('SWARM_MODE') ?: 'production',
'hostname' => getenv('SWARM_HOST') ?: null
),
'http_client_options' => array(
'timeout' => 5,
'hosts' => array() // optional, per-host overrides; host as key, array of options as value
),
'session' => array(
'cookie_lifetime' => 0, // session cookie lifetime to use when remember me isn't checked
'remembered_cookie_lifetime' => 30*24*60*60 // session cookie lifetime to use when remember me is checked
),
'security' => array(
'require_login' => false, // if enabled only the login screen will be accessible for anonymous users
'disable_autojoin' => false, // if enabled user will not auto-join the swarm group on login
'https_strict' => false, // if enabled, we'll tell clients to pin on https for 30 days
'https_strict_redirect' => true, // if both https_strict and this are enabled; we meta-refresh HTTP to HTTPS
'https_port' => null, // optionally, specify a non-standard port to use for https
'emulate_ip_protections' => true, // if enabled, ip-based protections matching user's remote ip are applied
'disable_system_info' => false, // if enabled, system info is disabled (results in a 403 if accessed)
'csrf_exempt' => array('goto')
),
'git_fusion' => array(
'depot' => '.git-fusion',
'user' => 'git-fusion-user',
'reown' => array( // git-fusion commits as its user then re-owns the change to the real author
'retries' => 20, // we'll retry processing up to this many times to get the actual author
'max_wait' => 60 // the delay between tries starts at 2 seconds and grows up to this limit
)
),
'css' => array(
'/build/min.css' => array(
'/vendor/bootstrap/css/bootstrap.min.css',
'/vendor/prettify/prettify.css',
'/swarm/css/style.css'
)
),
'p4' => array(
'slow_command_logging' => array(
3, // commands without a specific rule get a 3 second limit
10 => array('print', 'shelve', 'submit', 'sync', 'unshelve')
),
'max_changelist_files' => 1000 // limit the number of files displayed in a change or a review
),
'js' => array(
'/build/min.js' => array(
'/vendor/jquery/jquery-1.11.1.min.js',
'/vendor/jquery-sortable/jquery-sortable-min.js',
'/vendor/bootstrap/js/bootstrap.min.js',
'/vendor/diff_match_patch/diff_match_patch.js',
'/vendor/jquery.expander/jquery.expander.min.js',
'/vendor/jquery.timeago/jquery.timeago.js',
'/vendor/jsrender/jsrender.js',
'/vendor/prettify/prettify.js',
'/vendor/jed/jed.js',
'/swarm/js/jquery-plugins.js',
'/swarm/js/bootstrap-extensions.js',
'/swarm/js/application.js',
'/swarm/js/activity.js',
'/swarm/js/users.js',
'/swarm/js/projects.js',
'/swarm/js/files.js',
'/swarm/js/changes.js',
'/swarm/js/comments.js',
'/swarm/js/attachments.js',
'/swarm/js/reviews.js',
'/swarm/js/jobs.js',
'/swarm/js/3dviewer.js',
'/swarm/js/i18n.js',
'/swarm/js/init.js'
)
),
'router' => array(
'routes' => array(
'about' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/about[/]',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'about',
),
),
),
'goto' => array(
'type' => 'Application\Router\Regex',
'options' => array(
'regex' => '/(@+)?(?P<id>.+)',
'spec' => '/@%id%',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'goto',
'id' => null
),
),
'priority' => -1000 // we'll catch anything that falls through by setting a late priority
),
'info' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/info[/]',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'info',
),
),
),
'archive' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/info/archive[/]',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'archive',
),
),
),
'log' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/info/log[/]',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'log',
),
),
),
'phpinfo' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/info/phpinfo[/]',
'defaults' => array(
'controller' => 'Application\Controller\Index',
'action' => 'phpinfo',
),
),
),
'upgrade' => array(
'type' => 'Zend\Mvc\Router\Http\Segment',
'options' => array(
'route' => '/upgrade[/]',
'defaults' => array(
'controller' => 'Reviews\Controller\Index',
'action' => 'upgrade',
),
),
),
),
),
'controllers' => array(
'invokables' => array(
'Application\Controller\Index' => 'Application\Controller\IndexController'
),
),
'service_manager' => array(
'aliases' => array(
'translator' => 'MvcTranslator',
),
'factories' => array(
'logger' => function ($services) {
// @todo update to use logger factory when available
// see PR #2725 (milestone 2.1.0)
$config = $services->get('config');
$logger = new Zend\Log\Logger;
$file = isset($config['log']['file']) ? $config['log']['file'] : null;
// if a file was specified but doesn't exist attempt to create
// it (unless we are running on the command line).
// for cli usage we don't want to risk the log being owned by
// a user other than the web-server so we won't touch it here.
if ($file && !file_exists($file) && php_sapi_name() !== 'cli') {
touch($file);
}
// if a writable file was specified use it, otherwise just use null
if ($file && is_writable($file)) {
$writer = new Zend\Log\Writer\Stream($file);
if (isset($config['log']['priority'])) {
$writer->addFilter((int) $config['log']['priority']);
}
$logger->addWriter($writer);
} else {
$logger->addWriter(new Zend\Log\Writer\Null);
}
// register a custom error handler; we can not use the logger's as
// it would log 'context' which gets vastly too noisy
set_error_handler(
function ($level, $message, $file, $line) use ($logger) {
if (error_reporting() & $level) {
$map = Zend\Log\Logger::$errorPriorityMap;
$logger->log(
isset($map[$level]) ? $map[$level] : $logger::INFO,
$message,
array(
'errno' => $level,
'file' => $file,
'line' => $line
)
);
}
return false;
}
);
return $logger;
},
'p4' => function ($services) {
// if we have a logged in user, we want to use their connection
// to perforce. otherwise, we will use the admin connection
if ($services->get('permissions')->is('authenticated')) {
return $services->get('p4_user');
}
// doesn't appear anyone is logged in, run as admin
return $services->get('p4_admin');
},
'p4_admin' => function ($services) {
$config = $services->get('config') + array('p4' => array());
$p4 = (array) $config['p4'];
$factory = new \Application\Connection\ConnectionFactory($p4);
return $factory->createService($services);
},
'p4_user' => function ($services) {
$config = $services->get('config') + array('p4' => array());
$p4 = (array) $config['p4'];
$auth = $services->get('auth');
$identity = $auth->hasIdentity() ? (array) $auth->getIdentity() : array();
// can't get a user specific connection if user is not authenticated
if (!isset($identity['id']) || !strlen($identity['id'])) {
throw new \Application\Permissions\Exception\UnauthorizedException;
}
// tweak the 'p4' settings to use the users id/ticket and ensure password isn't present
$p4['user'] = $identity['id'];
$p4['ticket'] = isset($identity['ticket']) ? $identity['ticket'] : null;
unset($p4['password']);
$factory = new \Application\Connection\ConnectionFactory($p4);
$connection = $factory->createService($services);
// share a cache with the 'admin' connection
$connection->setService(
'cache',
function () use ($services) {
return $services->get('p4_admin')->getService('cache');
}
);
// verify the user is authenticated.
// if the ticket/password is invalid, try to clean up the auth and user
// services to reflect the anonymous state (someone may have fetched them
// before us leaving them otherwise in a bad state).
if (!$connection->isAuthenticated()) {
// if our bad connection is the default; clear it
if (P4\Connection\Connection::hasDefaultConnection()
&& P4\Connection\Connection::getDefaultConnection() === $connection
) {
P4\Connection\Connection::clearDefaultConnection();
}
// if using session-based auth, empty/destroy the session
if ($auth->getStorage() instanceof Zend\Authentication\Storage\Session) {
$session = $services->get('session');
$session->start();
$auth->getStorage()->write(null);
$session->destroy(array('send_expire_cookie' => true, 'clear_storage' => true));
$session->writeClose();
}
// if the user service is already instantiated, clear
// the existing object; we want to try and clear out
// anyone who already has a copy.
$registered = $services->getRegisteredServices();
if (in_array('user', $registered['instances'])) {
$services->get('user')
->setId(null)
->setEmail(null)
->setFullName(null)
->setJobView(null)
->setReviews(array())
->setConfig(new \Users\Model\Config);
}
throw new \Application\Permissions\Exception\UnauthorizedException;
}
return $connection;
},
'session' => function ($services) {
$config = $services->get('config') + array('session' => array());
$strict = isset($config['security']['https_strict']) && $config['security']['https_strict'];
$config = $config['session'] + array(
'name' => null,
'save_path' => null,
'cookie_lifetime' => null,
'remembered_cookie_lifetime' => null
);
// detect if we're on https, if we are (or we're strict) we'll set the cookie to secure
$request = $services->get('request');
$https = $request instanceof Zend\Http\Request && $request->getUri()->getScheme() == 'https';
// by default, relocate session storage if we can to avoid mixing with
// other php apps using different/default session clean settings.
$config['save_path'] = $config['save_path'] ?: DATA_PATH . '/sessions';
is_dir($config['save_path']) ?: @mkdir($config['save_path'], 0700, true);
if (!is_writable($config['save_path'])) {
unset($config['save_path']);
}
// by default, we name the session id SWARM and, if its running on a
// non-standard port, we add the port number. This allows separate
// Swarm instances to run on a given domain using different ports.
if (!$config['name']) {
// we try to extract the port from the HTTP_HOST if possible.
// if we fail to find it there we fall back to the SERVER_PORT variable
// SERVER_PORT is fairly certain to be present but known to report 80
// even when another port is in use under some apache configurations.
$server = $_SERVER + array('HTTP_HOST' => '', 'SERVER_PORT' => null);
preg_match('/:(?P<port>[0-9]+)$/', $server['HTTP_HOST'], $matches);
$port = isset($matches['port']) && $matches['port']
? $matches['port']
: $server['SERVER_PORT'];
$config['name'] = 'SWARM'
. ($port && $port != 80 && $port != 443 ? '-' . $port : '');
}
// verify the session isn't already started (shouldn't be) and adjust
// the settings. attempting an adjustment post start produces errors.
$sessionConfig = new \Zend\Session\Config\SessionConfig;
if (!session_id()) {
// if the user has a 'remember me' cookie utilize the 'remembered' cookie lifetime
// note we have to clear the made up remembered_cookie_lifetime regardless as it
// would cause an exception if it makes it into the session config.
if (isset($_COOKIE['remember']) && $_COOKIE['remember']) {
$config['cookie_lifetime'] = $config['remembered_cookie_lifetime'];
}
unset($config['remembered_cookie_lifetime']);
// set the session config by mixing any user provided config
// values with our defaults
$sessionConfig->setOptions(
$config +
array(
'cookie_httponly' => true,
'cookie_secure' => $https || $strict,
'gc_probability' => 1,
'gc_divisor' => 100,
'gc_maxlifetime' => 24*60*60 * 30 // 1 month
)
);
}
$session = new Application\Session\SessionManager($sessionConfig);
// a couple conditions require the session id pre-start, get it if possible
$sessionName = $sessionConfig->getOption('name');
$sessionId = isset($_COOKIE[$sessionName]) ? $_COOKIE[$sessionName] : null;
// if we have no session cookie, no need to deal with session expiry or
// read current values from disk; just bail!
if (!strlen($sessionId)) {
return $session;
}
// we want to actually enforce the gc lifetime for file-based sessions.
// to support this, pull the mtime from the session file before we start.
$sessionFile = strlen($sessionId) && $sessionConfig->getOption('save_handler') == 'files'
? $sessionConfig->getOption('save_path') . '/sess_' . $sessionId
: false;
$sessionTime = $sessionFile && file_exists($sessionFile) ? filemtime($sessionFile) : false;
// ensure the session is started (to populate session storage data)
// but promptly close it to minimize locking - anytime we need
// to update the session later, we need to explicitly open/close it.
$session->start();
// if we found a session file mod-time and its expired, destroy the session
if ($sessionTime
&& (time() - $sessionTime) > $sessionConfig->getOption('gc_maxlifetime')
) {
$session->destroy(array('send_expire_cookie' => true, 'clear_storage' => true));
}
$session->writeClose();
return $session;
},
'permissions' => function ($services) {
return new Application\Permissions\Permissions($services);
},
'ip_protects' => function ($services) {
$config = $services->get('config');
$remoteIp = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
$enabled = isset($config['security']['emulate_ip_protections'])
&& $config['security']['emulate_ip_protections'];
// create and configure ip protections emulation
$protections = new Application\Permissions\Protections;
$protections->setEnabled(false);
if ($enabled && $remoteIp) {
$p4 = $services->get('p4');
// determine whether connected server is case sensitive or case insensitive
// if we can't puzzle it out, treat is as case sensitive (more restrictive)
try {
$isCaseSensitive = $p4->isCaseSensitive();
} catch (P4\Exception $e) {
$isCaseSensitive = true;
}
// collect lines from the protections table to apply
// we take non-proxy rules for user's IP, but we also take proxy rules to
// express we treat Swarm as an intermediary
try {
$protectionsData = array_merge(
$p4->run('protects', array('-h', $remoteIp))->getData(),
$p4->run('protects', array('-h', 'proxy-' . $remoteIp))->getData()
);
// sort merged protections data to preserve their original order in the protections table
usort(
$protectionsData,
function (array $a, array $b) {
return (int) $a['line'] - (int) $b['line'];
}
);
$protections->setProtections($protectionsData, $isCaseSensitive);
$protections->setEnabled(true);
} catch (P4\Connection\Exception\CommandException $e) {
if (strpos($e->getMessage(), 'Protections table is empty.') === false) {
// we don't recognize the message, so re-throw the exception
throw $e;
}
}
}
return $protections;
},
'depot_storage' => function ($services) {
$config = $services->get('config');
$config = $config['depot_storage'] + array('base_path'=>null);
$depot = new Record\File\FileService($services->get('p4_admin'));
$depot->setConfig($config);
return $depot;
},
'changes_filter' => function ($services) {
return new Application\Permissions\RestrictedChanges($services->get('p4'));
},
'csrf' => function ($services) {
return new Application\Permissions\Csrf\Service($services);
},
'MvcTranslator' => function ($services) {
$config = $services->get('config');
$config = isset($config['translator']) ? $config['translator'] : array();
$translator = \Application\I18n\Translator::factory($config);
$translator->setEscaper(new \Application\Escaper\Escaper);
// add event listener for context fallback on missing translations
$translator->enableEventManager();
$translator->getEventManager()->attach(
$translator::EVENT_MISSING_TRANSLATION,
array($translator, 'handleMissingTranslation')
);
// establish default locale settings
$translator->setLocale($translator->getLocale() ?: 'en_US');
$translator->setFallbackLocale($translator->getFallbackLocale() ?: 'en_US');
// try to guess locale from browser language header (using intl if available)
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
&& (!isset($config['detect_locale']) || $config['detect_locale'] !== false)
) {
$locale = extension_loaded('intl')
? \Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE'])
: str_replace('-', '_', current(explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE'])));
// if we can't find an exact match, venture a guess based on language prefix
if (!$translator->isSupportedLocale($locale)) {
$language = current(preg_split('/[^a-z]/i', $locale));
$locale = $translator->isSupportedLanguage($language) ?: $locale;
}
$translator->setLocale(strlen($locale) ? $locale : $translator->getLocale());
}
return $translator;
},
),
),
'translator' => array(
'locale' => 'en_US',
'detect_locale' => true,
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => BASE_PATH . '/language',
'pattern' => '%s/default.mo',
),
),
),
'view_manager' => array(
'display_not_found_reason' => false,
'display_exceptions' => false,
'doctype' => 'HTML5',
'not_found_template' => 'error/index',
'exception_template' => 'error/index',
'template_map' => array(
'layout/layout' => __DIR__ . '/../view/layout/layout.phtml',
'layout/toolbar' => __DIR__ . '/../view/layout/toolbar.phtml',
'application/index/index' => __DIR__ . '/../view/application/index/index.phtml',
'error/index' => __DIR__ . '/../view/error/index.phtml',
),
'template_path_stack' => array(
__DIR__ . '/../view',
),
'strategies' => array(
'ViewJsonStrategy', 'ViewFeedStrategy'
),
),
'log' => array(
'file' => DATA_PATH . '/log',
'priority' => 3 // just log errors by default
),
'view_helpers' => array(
'invokables' => array(
'breadcrumbs' => 'Application\View\Helper\Breadcrumbs',
'bodyClass' => 'Application\View\Helper\BodyClass',
'csrf' => 'Application\View\Helper\Csrf',
'escapeFullUrl' => 'Application\View\Helper\EscapeFullUrl',
'headLink' => 'Application\View\Helper\HeadLink',
'headScript' => 'Application\View\Helper\HeadScript',
'linkify' => 'Application\View\Helper\Linkify',
'permissions' => 'Application\View\Helper\Permissions',
'preformat' => 'Application\View\Helper\Preformat',
'qualifiedUrl' => 'Application\View\Helper\QualifiedUrl',
'request' => 'Application\View\Helper\Request',
'shortenStackTrace' => 'Application\View\Helper\ShortenStackTrace',
'truncate' => 'Application\View\Helper\Truncate',
'utf8Filter' => 'Application\View\Helper\Utf8Filter',
'wordify' => 'Application\View\Helper\Wordify',
'wordWrap' => 'Application\View\Helper\WordWrap',
't' => 'Application\View\Helper\Translate',
'te' => 'Application\View\Helper\TranslateEscape',
'tp' => 'Application\View\Helper\TranslatePlural',
'tpe' => 'Application\View\Helper\TranslatePluralEscape',
),
),
'controller_plugins' => array(
'invokables' => array(
'Disconnect' => 'Application\Controller\Plugin\Disconnect'
)
),
'depot_storage' => array(
'base_path' => '//.swarm'
)
);