Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
index 660792ca53..59b04d895f 100644
--- a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
+++ b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php
@@ -1,153 +1,153 @@
<?php
final class PhabricatorManualActivitySetupCheck
extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$activities = id(new PhabricatorConfigManualActivity())->loadAll();
foreach ($activities as $activity) {
$type = $activity->getActivityType();
switch ($type) {
case PhabricatorConfigManualActivity::TYPE_REINDEX:
$this->raiseSearchReindexIssue();
break;
case PhabricatorConfigManualActivity::TYPE_IDENTITIES:
$this->raiseRebuildIdentitiesIssue();
break;
default:
}
}
}
private function raiseSearchReindexIssue() {
$activity_name = pht('Rebuild Search Index');
$activity_summary = pht(
'The search index algorithm has been updated and the index needs '.
'be rebuilt.');
$message = array();
$message[] = pht(
'The indexing algorithm for the fulltext search index has been '.
'updated and the index needs to be rebuilt. Until you rebuild the '.
'index, global search (and other fulltext search) will not '.
'function correctly.');
$message[] = pht(
'You can rebuild the search index while the server is running.');
$message[] = pht(
'To rebuild the index, run this command:');
$message[] = phutil_tag(
'pre',
array(),
(string)csprintf(
'$ ./bin/search index --all --force --background'));
$message[] = pht(
'You can find more information about rebuilding the search '.
'index here: %s',
phutil_tag(
'a',
array(
- 'href' => 'https://phurl.io/u/reindex',
+ 'href' => 'https://secure.phabricator.com/T11932',
'target' => '_blank',
),
- 'https://phurl.io/u/reindex'));
+ 'https://secure.phabricator.com/T11932'));
$message[] = pht(
'After rebuilding the index, run this command to clear this setup '.
'warning:');
$message[] = phutil_tag(
'pre',
array(),
'$ ./bin/config done reindex');
$activity_message = phutil_implode_html("\n\n", $message);
$this->newIssue('manual.reindex')
->setName($activity_name)
->setSummary($activity_summary)
->setMessage($activity_message);
}
private function raiseRebuildIdentitiesIssue() {
$activity_name = pht('Rebuild Repository Identities');
$activity_summary = pht(
'The mapping from VCS users to %s users has changed '.
'and must be rebuilt.',
PlatformSymbols::getPlatformServerName());
$message = array();
$message[] = pht(
'The way VCS activity is attributed %s user accounts has changed.',
PlatformSymbols::getPlatformServerName());
$message[] = pht(
'There is a new indirection layer between the strings that appear as '.
'VCS authors and committers (such as "John Developer '.
'<johnd@bigcorp.com>") and the user account that gets associated '.
'with VCS commits.');
$message[] = pht(
'This change supports situations where users are incorrectly '.
'associated with commits because the software makes a bad guess '.
'about how the VCS string maps to a user account. '.
'This also helps with situations where existing repositories are '.
'imported without having created accounts for all the committers to '.
'that repository. Until you rebuild these repository identities, you '.
'are likely to encounter problems with features which rely on the '.
'existence of these identities.');
$message[] = pht(
'You can rebuild repository identities while the server is running.');
$message[] = pht(
'To rebuild identities, run this command:');
$message[] = phutil_tag(
'pre',
array(),
(string)csprintf(
'$ ./bin/repository rebuild-identities --all-repositories'));
$message[] = pht(
'You can find more information about this new identity mapping '.
'here: %s',
phutil_tag(
'a',
array(
- 'href' => 'https://phurl.io/u/repoIdentities',
+ 'href' => 'https://secure.phabricator.com/T12164',
'target' => '_blank',
),
- 'https://phurl.io/u/repoIdentities'));
+ 'https://secure.phabricator.com/T12164'));
$message[] = pht(
'After rebuilding repository identities, run this command to clear '.
'this setup warning:');
$message[] = phutil_tag(
'pre',
array(),
'$ ./bin/config done identities');
$activity_message = phutil_implode_html("\n\n", $message);
$this->newIssue('manual.identities')
->setName($activity_name)
->setSummary($activity_summary)
->setMessage($activity_message);
}
}
diff --git a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
index 02c1134dc3..322ad26061 100644
--- a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
@@ -1,150 +1,150 @@
<?php
final class PhabricatorPHPPreflightSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
public function isPreflightCheck() {
return true;
}
protected function executeChecks() {
$version = phpversion();
if (version_compare($version, 7, '>=') &&
version_compare($version, 7.1, '<')) {
$message = pht(
'You are running PHP version %s. PHP versions between 7.0 and 7.1 '.
'are not supported'.
"\n\n".
- 'PHP removed reqiured signal handling features in '.
+ 'PHP removed required signal handling features in '.
'PHP 7.0, and did not restore an equivalent mechanism until PHP 7.1.'.
"\n\n".
'Upgrade to PHP 7.1 or newer (recommended) or downgrade to an older '.
'version of PHP 5 (discouraged).',
$version);
$this->newIssue('php.version7')
->setIsFatal(true)
->setName(pht('PHP 7.0-7.1 Not Supported'))
->setMessage($message)
->addLink(
- 'https://phurl.io/u/php7',
+ 'https://secure.phabricator.com/T12101',
pht('PHP 7 Compatibility Information'));
return;
}
// TODO: This can be removed entirely because the minimum PHP version is
// now PHP 5.5, which does not have safe mode.
$safe_mode = ini_get('safe_mode');
if ($safe_mode) {
$message = pht(
"You have '%s' enabled in your PHP configuration, but this software ".
"will not run in safe mode. Safe mode has been deprecated in PHP 5.3 ".
"and removed in PHP 5.4.\n\nDisable safe mode to continue.",
'safe_mode');
$this->newIssue('php.safe_mode')
->setIsFatal(true)
->setName(pht('Disable PHP %s', 'safe_mode'))
->setMessage($message)
->addPHPConfig('safe_mode');
return;
}
// Check for `disable_functions` or `disable_classes`. Although it's
// possible to disable a bunch of functions (say, `array_change_key_case()`)
// and classes and still have Phabricator work fine, it's unreasonably
// difficult for us to be sure we'll even survive setup if these options
// are enabled. Phabricator needs access to the most dangerous functions,
// so there is no reasonable configuration value here which actually
// provides a benefit while guaranteeing Phabricator will run properly.
$disable_options = array('disable_functions', 'disable_classes');
foreach ($disable_options as $disable_option) {
$disable_value = ini_get($disable_option);
if ($disable_value) {
// By default Debian installs the pcntl extension but disables all of
// its functions using configuration. Whitelist disabling these
// functions so that Debian PHP works out of the box (we do not need to
// call these functions from the web UI). This is pretty ridiculous but
// it's not the users' fault and they haven't done anything crazy to
// get here, so don't make them pay for Debian's unusual choices.
// See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=605571
$fatal = true;
if ($disable_option == 'disable_functions') {
$functions = preg_split('/[, ]+/', $disable_value);
$functions = array_filter($functions);
foreach ($functions as $k => $function) {
if (preg_match('/^pcntl_/', $function)) {
unset($functions[$k]);
}
}
if (!$functions) {
$fatal = false;
}
}
if ($fatal) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with this software. Remove ".
"'%s' from your configuration to continue.",
$disable_option,
$disable_option);
$this->newIssue('php.'.$disable_option)
->setIsFatal(true)
->setName(pht('Remove PHP %s', $disable_option))
->setMessage($message)
->addPHPConfig($disable_option);
}
}
}
$overload_option = 'mbstring.func_overload';
$func_overload = ini_get($overload_option);
if ($func_overload) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with this software. Disable ".
"'%s' in your PHP configuration to continue.",
$overload_option,
$overload_option);
$this->newIssue('php'.$overload_option)
->setIsFatal(true)
->setName(pht('Disable PHP %s', $overload_option))
->setMessage($message)
->addPHPConfig($overload_option);
}
$open_basedir = ini_get('open_basedir');
if (strlen($open_basedir)) {
// If `open_basedir` is set, just fatal. It's technically possible for
// us to run with certain values of `open_basedir`, but: we can only
// raise fatal errors from preflight steps, so we'd have to do this check
// in two parts to support fatal and advisory versions; it's much simpler
// to just fatal instead of trying to test all the different things we
// may need to access in the filesystem; and use of this option seems
// rare (particularly in supported environments).
$message = pht(
"Your server is configured with '%s', which prevents this software ".
"from opening files it requires access to.\n\n".
"Disable this setting to continue.",
'open_basedir');
$issue = $this->newIssue('php.open_basedir')
->setName(pht('Disable PHP %s', 'open_basedir'))
->addPHPConfig('open_basedir')
->setIsFatal(true)
->setMessage($message);
}
}
}
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
index b7b512156f..d8f53538e4 100644
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -1,1101 +1,1101 @@
<?php
final class PhabricatorApplicationSearchController
extends PhabricatorSearchBaseController {
private $searchEngine;
private $navigation;
private $queryKey;
private $preface;
private $activeQuery;
public function setPreface($preface) {
$this->preface = $preface;
return $this;
}
public function getPreface() {
return $this->preface;
}
public function setQueryKey($query_key) {
$this->queryKey = $query_key;
return $this;
}
protected function getQueryKey() {
return $this->queryKey;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
protected function getNavigation() {
return $this->navigation;
}
public function setSearchEngine(
PhabricatorApplicationSearchEngine $search_engine) {
$this->searchEngine = $search_engine;
return $this;
}
protected function getSearchEngine() {
return $this->searchEngine;
}
protected function getActiveQuery() {
if (!$this->activeQuery) {
throw new Exception(pht('There is no active query yet.'));
}
return $this->activeQuery;
}
protected function validateDelegatingController() {
$parent = $this->getDelegatingController();
if (!$parent) {
throw new Exception(
pht('You must delegate to this controller, not invoke it directly.'));
}
$engine = $this->getSearchEngine();
if (!$engine) {
throw new PhutilInvalidStateException('setEngine');
}
$engine->setViewer($this->getRequest()->getUser());
$parent = $this->getDelegatingController();
}
public function processRequest() {
$this->validateDelegatingController();
$query_action = $this->getRequest()->getURIData('queryAction');
if ($query_action == 'export') {
return $this->processExportRequest();
}
if ($query_action === 'customize') {
return $this->processCustomizeRequest();
}
$key = $this->getQueryKey();
if ($key == 'edit') {
return $this->processEditRequest();
} else {
return $this->processSearchRequest();
}
}
private function processSearchRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$user = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved_query);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
}
$named_query = null;
$run_query = true;
$query_key = $this->queryKey;
if ($this->queryKey == 'advanced') {
$run_query = false;
$query_key = $request->getStr('query');
} else if (!phutil_nonempty_string($this->queryKey)) {
$found_query_data = false;
if ($request->isHTTPGet() || $request->isQuicksand()) {
// If this is a GET request and it has some query data, don't
// do anything unless it's only before= or after=. We'll build and
// execute a query from it below. This allows external tools to build
// URIs like "/query/?users=a,b".
$pt_data = $request->getPassthroughRequestData();
$exempt = array(
'before' => true,
'after' => true,
'nux' => true,
'overheated' => true,
);
foreach ($pt_data as $pt_key => $pt_value) {
if (isset($exempt[$pt_key])) {
continue;
}
$found_query_data = true;
break;
}
}
if (!$found_query_data) {
// Otherwise, there's no query data so just run the user's default
// query for this application.
$query_key = $engine->getDefaultQueryKey();
}
}
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($user)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
return new Aphront404Response();
}
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else {
$saved_query = $engine->buildSavedQueryFromRequest($request);
// Save the query to generate a query key, so "Save Custom Query..." and
// other features like "Bulk Edit" and "Export Data" work correctly.
$engine->saveQuery($saved_query);
}
$this->activeQuery = $saved_query;
$nav->selectFilter(
'query/'.$saved_query->getQueryKey(),
'query/advanced');
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getPath());
$engine->buildSearchForm($form, $saved_query);
$errors = $engine->getErrors();
if ($errors) {
$run_query = false;
}
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Search'));
if ($run_query && !$named_query && $user->isLoggedIn()) {
$save_button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY)
->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
->setText(pht('Save Query'))
->setIcon('fa-bookmark');
$submit->addButton($save_button);
}
$form->appendChild($submit);
$body = array();
if ($this->getPreface()) {
$body[] = $this->getPreface();
}
if ($named_query) {
$title = $named_query->getQueryName();
} else {
$title = pht('Advanced Search');
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setProfileHeader(true);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addClass('application-search-results');
if ($run_query || $named_query) {
$box->setShowHide(
pht('Edit Query'),
pht('Hide Query'),
$form,
$this->getApplicationURI('query/advanced/?query='.$query_key),
(!$named_query ? true : false));
} else {
$box->setForm($form);
}
$body[] = $box;
$more_crumbs = null;
if ($run_query) {
$exec_errors = array();
$box->setAnchor(
id(new PhabricatorAnchorView())
->setAnchorName('R'));
try {
$engine->setRequest($request);
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->readFromRequest($request);
$query->setReturnPartialResultsOnOverheat(true);
$objects = $engine->executeQuery($query, $pager);
$force_nux = $request->getBool('nux');
if (!$objects || $force_nux) {
$nux_view = $this->renderNewUserView($engine, $force_nux);
} else {
$nux_view = null;
}
$is_overflowing =
$pager->willShowPagingControls() &&
$engine->getResultBucket($saved_query);
$force_overheated = $request->getBool('overheated');
$is_overheated = $query->getIsOverheated() || $force_overheated;
if ($nux_view) {
$box->appendChild($nux_view);
} else {
$list = $engine->renderResults($objects, $saved_query);
if (!($list instanceof PhabricatorApplicationSearchResultView)) {
throw new Exception(
pht(
'SearchEngines must render a "%s" object, but this engine '.
'(of class "%s") rendered something else ("%s").',
'PhabricatorApplicationSearchResultView',
get_class($engine),
phutil_describe_type($list)));
}
if ($list->getObjectList()) {
$box->setObjectList($list->getObjectList());
}
if ($list->getTable()) {
$box->setTable($list->getTable());
}
if ($list->getInfoView()) {
$box->setInfoView($list->getInfoView());
}
if ($is_overflowing) {
$box->appendChild($this->newOverflowingView());
}
if ($list->getContent()) {
$box->appendChild($list->getContent());
}
if ($is_overheated) {
$box->appendChild($this->newOverheatedView($objects));
}
$result_header = $list->getHeader();
if ($result_header) {
$box->setHeader($result_header);
$header = $result_header;
}
$actions = $list->getActions();
if ($actions) {
foreach ($actions as $action) {
$header->addActionLink($action);
}
}
$use_actions = $engine->newUseResultsActions($saved_query);
// TODO: Eventually, modularize all this stuff.
$builtin_use_actions = $this->newBuiltinUseActions();
if ($builtin_use_actions) {
foreach ($builtin_use_actions as $builtin_use_action) {
$use_actions[] = $builtin_use_action;
}
}
if ($use_actions) {
$use_dropdown = $this->newUseResultsDropdown(
$saved_query,
$use_actions);
$header->addActionLink($use_dropdown);
}
$more_crumbs = $list->getCrumbs();
if ($pager->willShowPagingControls()) {
$pager_box = id(new PHUIBoxView())
->setColor(PHUIBoxView::GREY)
->addClass('application-search-pager')
->appendChild($pager);
$body[] = $pager_box;
}
}
} catch (PhabricatorTypeaheadInvalidTokenException $ex) {
$exec_errors[] = pht(
'This query specifies an invalid parameter. Review the '.
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorInvalidQueryCursorException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;
// merge them in and show everything.
foreach ($engine->getErrors() as $error) {
$exec_errors[] = $error;
}
$errors = $exec_errors;
}
if ($errors) {
$box->setFormErrors($errors, pht('Query Errors'));
}
$crumbs = $parent
->buildApplicationCrumbs()
->setBorder(true);
if ($more_crumbs) {
$query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
$crumbs->addTextCrumb($title, $query_uri);
foreach ($more_crumbs as $crumb) {
$crumbs->addCrumb($crumb);
}
} else {
$crumbs->addTextCrumb($title);
}
require_celerity_resource('application-search-view-css');
return $this->newPage()
->setTitle(pht('Query: %s', $title))
->setCrumbs($crumbs)
->setNavigation($nav)
->addClass('application-search-view')
->appendChild($body);
}
private function processExportRequest() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$request = $this->getRequest();
if (!$this->canExport()) {
return new Aphront404Response();
}
$query_key = $this->getQueryKey();
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
} else {
$saved_query = null;
}
if (!$saved_query) {
return new Aphront404Response();
}
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
if ($named_query) {
$filename = $named_query->getQueryName();
$sheet_title = $named_query->getQueryName();
} else {
$filename = $engine->getResultTypeDescription();
$sheet_title = $engine->getResultTypeDescription();
}
$filename = phutil_utf8_strtolower($filename);
$filename = PhabricatorFile::normalizeFileName($filename);
$all_formats = PhabricatorExportFormat::getAllExportFormats();
$available_options = array();
$unavailable_options = array();
$formats = array();
$unavailable_formats = array();
foreach ($all_formats as $key => $format) {
if ($format->isExportFormatEnabled()) {
$available_options[$key] = $format->getExportFormatName();
$formats[$key] = $format;
} else {
$unavailable_options[$key] = pht(
'%s (Not Available)',
$format->getExportFormatName());
$unavailable_formats[$key] = $format;
}
}
$format_options = $available_options + $unavailable_options;
// Try to default to the format the user used last time. If you just
// exported to Excel, you probably want to export to Excel again.
$format_key = $this->readExportFormatPreference();
if (!isset($formats[$format_key])) {
$format_key = head_key($format_options);
}
// Check if this is a large result set or not. If we're exporting a
// large amount of data, we'll build the actual export file in the daemons.
$threshold = 1000;
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->setPageSize($threshold + 1);
$objects = $engine->executeQuery($query, $pager);
$object_count = count($objects);
$is_large_export = ($object_count > $threshold);
$errors = array();
$e_format = null;
if ($request->isFormPost()) {
$format_key = $request->getStr('format');
if (isset($unavailable_formats[$format_key])) {
$unavailable = $unavailable_formats[$format_key];
$instructions = $unavailable->getInstallInstructions();
$markup = id(new PHUIRemarkupView($viewer, $instructions))
->setRemarkupOption(
PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
false);
return $this->newDialog()
->setTitle(pht('Export Format Not Available'))
->appendChild($markup)
->addCancelButton($cancel_uri, pht('Done'));
}
$format = idx($formats, $format_key);
if (!$format) {
$e_format = pht('Invalid');
$errors[] = pht('Choose a valid export format.');
}
if (!$errors) {
$this->writeExportFormatPreference($format_key);
$export_engine = id(new PhabricatorExportEngine())
->setViewer($viewer)
->setSearchEngine($engine)
->setSavedQuery($saved_query)
->setTitle($sheet_title)
->setFilename($filename)
->setExportFormat($format);
if ($is_large_export) {
$job = $export_engine->newBulkJob($request);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
} else {
$file = $export_engine->exportFile();
return $file->newDownloadResponse();
}
}
}
$export_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('format')
->setLabel(pht('Format'))
->setError($e_format)
->setValue($format_key)
->setOptions($format_options));
if ($is_large_export) {
$submit_button = pht('Continue');
} else {
$submit_button = pht('Download Data');
}
return $this->newDialog()
->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
private function processEditRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
$named_queries = $engine->loadAllNamedQueries();
$can_global = $viewer->getIsAdmin();
$groups = array(
'personal' => array(
'name' => pht('Personal Saved Queries'),
'items' => array(),
'edit' => true,
),
'global' => array(
'name' => pht('Global Saved Queries'),
'items' => array(),
'edit' => $can_global,
),
);
foreach ($named_queries as $named_query) {
if ($named_query->isGlobal()) {
$group = 'global';
} else {
$group = 'personal';
}
$groups[$group]['items'][] = $named_query;
}
$default_key = $engine->getDefaultQueryKey();
$lists = array();
foreach ($groups as $group) {
$lists[] = $this->newQueryListView(
$group['name'],
$group['items'],
$default_key,
$group['edit']);
}
$crumbs = $parent
->buildApplicationCrumbs()
->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
->setBorder(true);
$nav->selectFilter('query/edit');
$header = id(new PHUIHeaderView())
->setHeader(pht('Saved Queries'))
->setProfileHeader(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($lists);
return $this->newPage()
->setTitle(pht('Saved Queries'))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function newQueryListView(
$list_name,
array $named_queries,
$default_key,
$can_edit) {
$engine = $this->getSearchEngine();
$viewer = $this->getViewer();
$list = id(new PHUIObjectItemListView())
->setViewer($viewer);
if ($can_edit) {
$list_id = celerity_generate_unique_node_id();
$list->setID($list_id);
Javelin::initBehavior(
'search-reorder-queries',
array(
'listID' => $list_id,
'orderURI' => '/search/order/'.get_class($engine).'/',
));
}
foreach ($named_queries as $named_query) {
$class = get_class($engine);
$key = $named_query->getQueryKey();
$item = id(new PHUIObjectItemView())
->setHeader($named_query->getQueryName())
->setHref($engine->getQueryResultsPageURI($key));
if ($named_query->getIsDisabled()) {
if ($can_edit) {
$item->setDisabled(true);
} else {
// If an item is disabled and you don't have permission to edit it,
// just skip it.
continue;
}
}
if ($can_edit) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
$icon = 'fa-plus';
$disable_name = pht('Enable');
} else {
$icon = 'fa-times';
if ($named_query->getIsBuiltin()) {
$disable_name = pht('Disable');
} else {
$disable_name = pht('Delete');
}
}
if ($named_query->getID()) {
$disable_href = '/search/delete/id/'.$named_query->getID().'/';
} else {
$disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($icon)
->setHref($disable_href)
->setRenderNameAsTooltip(true)
->setName($disable_name)
->setWorkflow(true));
}
$default_disabled = $named_query->getIsDisabled();
$default_icon = 'fa-thumb-tack';
if ($default_key === $key) {
$default_color = 'green';
} else {
$default_color = null;
}
$item->addAction(
id(new PHUIListItemView())
->setIcon("{$default_icon} {$default_color}")
->setHref('/search/default/'.$key.'/'.$class.'/')
->setRenderNameAsTooltip(true)
->setName(pht('Make Default'))
->setWorkflow(true)
->setDisabled($default_disabled));
if ($can_edit) {
if ($named_query->getIsBuiltin()) {
$edit_icon = 'fa-lock lightgreytext';
$edit_disabled = true;
$edit_name = pht('Builtin');
$edit_href = null;
} else {
$edit_icon = 'fa-pencil';
$edit_disabled = false;
$edit_name = pht('Edit');
$edit_href = '/search/edit/id/'.$named_query->getID().'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($edit_icon)
->setHref($edit_href)
->setRenderNameAsTooltip(true)
->setName($edit_name)
->setDisabled($edit_disabled));
}
$item->setGrippable($can_edit);
$item->addSigil('named-query');
$item->setMetadata(
array(
'queryKey' => $named_query->getQueryKey(),
));
$list->addItem($item);
}
$list->setNoDataString(pht('No saved queries.'));
return id(new PHUIObjectBoxView())
->setHeaderText($list_name)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
}
public function buildApplicationMenu() {
$menu = $this->getDelegatingController()
->buildApplicationMenu();
if ($menu instanceof PHUIApplicationMenuView) {
$menu->setSearchEngine($this->getSearchEngine());
}
return $menu;
}
private function buildNavigation() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$nav = id(new AphrontSideNavFilterView())
->setUser($viewer)
->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine->addNavigationItems($nav->getMenu());
return $nav;
}
private function renderNewUserView(
PhabricatorApplicationSearchEngine $engine,
$force_nux) {
// Don't render NUX if the user has clicked away from the default page.
if (phutil_nonempty_string($this->getQueryKey())) {
return null;
}
// Don't put NUX in panels because it would be weird.
if ($engine->isPanelContext()) {
return null;
}
// Try to render the view itself first, since this should be very cheap
// (just returning some text).
$nux_view = $engine->renderNewUserView();
if (!$nux_view) {
return null;
}
$query = $engine->newQuery();
if (!$query) {
return null;
}
// Try to load any object at all. If we can, the application has seen some
// use so we just render the normal view.
if (!$force_nux) {
$object = $query
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->setReturnPartialResultsOnOverheat(true)
->execute();
if ($object) {
return null;
}
}
return $nux_view;
}
private function newUseResultsDropdown(
PhabricatorSavedQuery $query,
array $dropdown_items) {
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($dropdown_items as $dropdown_item) {
$action_list->addAction($dropdown_item);
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setText(pht('Use Results'))
->setIcon('fa-bars')
->setDropdownMenu($action_list)
->addClass('dropdown');
}
private function newOverflowingView() {
$message = pht(
'The query matched more than one page of results. Results are '.
'paginated before bucketing, so later pages may contain additional '.
'results in any bucket.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Buckets Overflowing'))
->setErrors(
array(
$message,
));
}
public static function newOverheatedError($has_results) {
$overheated_link = phutil_tag(
'a',
array(
- 'href' => 'https://phurl.io/u/overheated',
+ 'href' => 'https://secure.phabricator.com/T13274',
'target' => '_blank',
),
pht('Learn More'));
if ($has_results) {
$message = pht(
'This query took too long, so only some results are shown. %s',
$overheated_link);
} else {
$message = pht(
'This query took too long. %s',
$overheated_link);
}
return $message;
}
private function newOverheatedView(array $results) {
$message = self::newOverheatedError((bool)$results);
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Query Overheated'))
->setErrors(
array(
$message,
));
}
private function newBuiltinUseActions() {
$actions = array();
$request = $this->getRequest();
$viewer = $request->getUser();
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$engine = $this->getSearchEngine();
$engine_class = get_class($engine);
$query_key = $this->getActiveQuery()->getQueryKey();
$can_use = $engine->canUseInPanelContext();
$is_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDashboardApplication',
$viewer);
if ($can_use && $is_installed) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-dashboard')
->setName(pht('Add to Dashboard'))
->setWorkflow(true)
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
}
if ($this->canExport()) {
$export_uri = $engine->getExportURI($query_key);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Export Data'))
->setWorkflow(true)
->setHref($export_uri);
}
if ($is_dev) {
$engine = $this->getSearchEngine();
$nux_uri = $engine->getQueryBaseURI();
$nux_uri = id(new PhutilURI($nux_uri))
->replaceQueryParam('nux', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-user-plus')
->setName(pht('DEV: New User State'))
->setHref($nux_uri);
}
if ($is_dev) {
$overheated_uri = $this->getRequest()->getRequestURI()
->replaceQueryParam('overheated', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-fire')
->setName(pht('DEV: Overheated State'))
->setHref($overheated_uri);
}
return $actions;
}
private function canExport() {
$engine = $this->getSearchEngine();
if (!$engine->canExport()) {
return false;
}
// Don't allow logged-out users to perform exports. There's no technical
// or policy reason they can't, but we don't normally give them access
// to write files or jobs. For now, just err on the side of caution.
$viewer = $this->getViewer();
if (!$viewer->getPHID()) {
return false;
}
return true;
}
private function readExportFormatPreference() {
$viewer = $this->getViewer();
$export_key = PhabricatorExportFormatSetting::SETTINGKEY;
$value = $viewer->getUserSetting($export_key);
if (is_string($value)) {
return $value;
}
return '';
}
private function writeExportFormatPreference($value) {
$viewer = $this->getViewer();
$request = $this->getRequest();
if (!$viewer->isLoggedIn()) {
return;
}
$export_key = PhabricatorExportFormatSetting::SETTINGKEY;
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($export_key, $value);
$editor->applyTransactions($preferences, $xactions);
}
private function processCustomizeRequest() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$request = $this->getRequest();
$object_phid = $request->getStr('search.objectPHID');
$context_phid = $request->getStr('search.contextPHID');
// For now, the object can only be a dashboard panel, so just use a panel
// query explicitly.
$object = id(new PhabricatorDashboardPanelQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$object_name = pht('%s %s', $object->getMonogram(), $object->getName());
// Likewise, the context object can only be a dashboard.
if (strlen($context_phid)) {
$context = id(new PhabricatorDashboardQuery())
->setViewer($viewer)
->withPHIDs(array($context_phid))
->executeOne();
if (!$context) {
return new Aphront404Response();
}
} else {
$context = $object;
}
$done_uri = $context->getURI();
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved_query);
$query_key = $saved_query->getQueryKey();
} else {
$query_key = $this->getQueryKey();
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
} else {
$saved_query = null;
}
}
if (!$saved_query) {
return new Aphront404Response();
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->addHiddenInput('search.objectPHID', $object_phid)
->addHiddenInput('search.contextPHID', $context_phid)
->setAction($request->getPath());
$engine->buildSearchForm($form, $saved_query);
$errors = $engine->getErrors();
if ($request->isFormPost()) {
if (!$errors) {
$xactions = array();
// Since this workflow is currently used only by dashboard panels,
// we can hard-code how the edit works.
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType(
PhabricatorDashboardQueryPanelQueryTransaction::TRANSACTIONTYPE)
->setNewValue($query_key);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($object, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
return $this->newDialog()
->setTitle(pht('Customize Query: %s', $object_name))
->setErrors($errors)
->setWidth(AphrontDialogView::WIDTH_FULL)
->appendForm($form)
->addCancelButton($done_uri)
->addSubmitButton(pht('Save Changes'));
}
}
diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner
index c7c9818057..75338f149e 100644
--- a/src/docs/user/configuration/configuring_inbound_email.diviner
+++ b/src/docs/user/configuration/configuring_inbound_email.diviner
@@ -1,294 +1,294 @@
@title Configuring Inbound Email
@group config
This document contains instructions for configuring inbound email, so users
may interact with some Phorge applications via email.
Preamble
========
Phorge can process inbound mail in two general ways:
**Handling Replies**: When users reply to email notifications about changes,
Phorge can turn email into comments on the relevant discussion thread.
**Creating Objects**: You can configure an address like `bugs@yourcompany.com`
to create new objects (like tasks) when users send email.
In either case, users can interact with objects via mail commands to apply a
broader set of changes to objects beyond commenting. (For example, you can use
`!close` to close a task or `!priority` to change task priority.)
To configure inbound mail, you will generally:
- Configure some mail domain to submit mail to Phorge for processing.
- For handling replies, set `metamta.reply-handler-domain` in your
configuration.
- For handling email that creates objects, configure inbound addresses in the
relevant application.
See below for details on each of these steps.
Configuration Overview
======================
Usually, the most challenging part of configuring inbound mail is getting mail
delivered to Phorge for processing. This step can be made much easier if
you use a third-party mail service which can submit mail to Phorge via
webhooks.
Some available approaches for delivering mail to Phorge are:
| Receive Mail With | Setup | Cost | Notes |
|--------|-------|------|-------|
| Postmark | Easy | Cheap | Recommended |
| SendGrid | Easy | Cheap | |
| Mailgun | Easy | Cheap | Discouraged |
| Local MTA | Difficult | Free | Discouraged |
The remainder of this document walks through configuring Phorge to
receive mail, and then configuring your chosen transport to deliver mail
to Phorge.
Configuring "Reply" Email
=========================
By default, Phorge uses a `noreply@phorge.example.com` email address
as the "From" address when it sends mail. The exact address it uses can be
configured with `metamta.default-address`.
When a user takes an action that generates mail, Phorge sets the
"Reply-To" address for the mail to that user's name and address. This means
that users can reply to email to discuss changes, but: the conversation won't
be recorded in Phorge; and users will not be able to use email commands
to take actions or make edits.
To change this behavior so that users can interact with objects in Phorge
over email, change the configuration key `metamta.reply-handler-domain` to some
domain you configure according to the instructions below, e.g.
`phorge.example.com`. Once you set this key, email will use a
"Reply-To" like `T123+273+af310f9220ad@phorge.example.com`, which -- when
configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Differential revisions, Maniphest tasks,
etc. over email.
If you don't want Phorge to take up an entire domain (or subdomain) you
can configure a general prefix so you can use a single mailbox to receive mail
on. To make use of this set `metamta.single-reply-handler-prefix` to the
prefix of your choice, and Phorge will prepend this to the "Reply-To"
mail address. This works because everything up to the first (optional) '+'
character in an email address is considered the receiver, and everything
after is essentially ignored.
Configuring "Create" Email
==========================
You can set up application email addresses to allow users to create objects via
email. For example, you could configure `bugs@phorge.example.com` to
create a Maniphest task out of any email which is sent to it.
You can find application email settings for each application at:
{nav icon=home, name=Home >
Applications >
type=instructions, name="Select an Application" >
icon=cog, name=Configure}
Not all applications support creating objects via email.
In some applications, including Maniphest, you can also configure Herald rules
with the `[ Content source ]` and/or `[ Receiving email address ]` fields to
route or handle objects based on which address mail was sent to.
You'll also need to configure the actual mail domain to submit mail to
Phorge by following the instructions below. Phorge will let you add
any address as an application address, but can only process mail which is
actually delivered to it.
Security
========
The email reply channel is "somewhat" authenticated. Each reply-to address is
unique to the recipient and includes a hash of user information and a unique
object ID, so it can only be used to update that object and only be used to act
on behalf of the recipient.
However, if an address is leaked (which is fairly easy -- for instance,
forwarding an email will leak a live reply address, or a user might take a
screenshot), //anyone// who can send mail to your reply-to domain may interact
with the object the email relates to as the user who leaked the mail. Because
the authentication around email has this weakness, some actions (like accepting
revisions) are not permitted over email.
This implementation is an attempt to balance utility and security, but makes
some sacrifices on both sides to achieve it because of the difficulty of
authenticating senders in the general case (e.g., where you are an open source
project and need to interact with users whose email accounts you have no control
over).
You can also set `metamta.public-replies`, which will change how Phorge
delivers email. Instead of sending each recipient a unique mail with a personal
reply-to address, it will send a single email to everyone with a public reply-to
address. This decreases security because anyone who can spoof a "From" address
can act as another user, but increases convenience if you use mailing lists and,
practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
If you enable application email addresses, those addresses also use the weaker
"From" authentication mechanism.
NOTE: Phorge does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a
reasonable mechanism exists to provide sender verification (e.g., DKIM
signatures are sufficient to authenticate the sender under your configuration,
or you are willing to require all users to sign their email), file a feature
request.
Testing and Debugging Inbound Email
===================================
You can use the `bin/mail` utility to test and review inbound mail. This can
help you determine if mail is being delivered to Phorge or not:
phorge/ $ ./bin/mail list-inbound # List inbound messages.
phorge/ $ ./bin/mail show-inbound # Show details about a message.
You can also test receiving mail, but note that this just simulates receiving
the mail and doesn't send any information over the network. It is
primarily aimed at developing email handlers: it will still work properly
if your inbound email configuration is incorrect or even disabled.
phorge/ $ ./bin/mail receive-test # Receive test message.
Run `bin/mail help <command>` for detailed help on using these commands.
Mailgun Setup
=============
To use Mailgun, you need a Mailgun account. You can sign up at
<http://www.mailgun.com>. Provided you have such an account, configure it
like this:
- Configure a mail domain according to Mailgun's instructions.
- Add a Mailgun route with a `catch_all()` rule which takes the action
`forward("https://phorge.example.com/mail/mailgun/")`. Replace the
example domain with your actual domain.
- Configure a mailer in `cluster.mailers` with your Mailgun API key.
Use of Mailgun is discouraged because of concerns that they may not be a
-trustworthy custodian of sensitive data. See <https://phurl.io/u/mailgun> for
-discussion and context.
+trustworthy custodian of sensitive data.
+See <https://secure.phabricator.com/T13669> for discussion and context.
Postmark Setup
==============
To process inbound mail from Postmark, configure this URI as your inbound
webhook URI in the Postmark control panel:
```
https://<phorge.yourdomain.com>/mail/postmark/
```
See also the Postmark section in @{article:Configuring Outbound Email} for
discussion of the remote address whitelist used to verify that requests this
endpoint receives are authentic requests originating from Postmark.
SendGrid Setup
==============
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
inbound email. Provided you have such an account, configure it like this:
- Configure an MX record according to SendGrid's instructions, i.e. add
`phorge.example.com MX 10 mx.sendgrid.net.` or similar.
- Go to the "Parse Incoming Emails" page on SendGrid
(<http://sendgrid.com/developer/reply>) and add the domain as the
"Hostname".
- Add the URL `https://phorge.example.com/mail/sendgrid/` as the "Url",
using your domain (and HTTP instead of HTTPS if you are not configured with
SSL).
- If you get an error that the hostname "can't be located or verified", it
means your MX record is either incorrectly configured or hasn't propagated
yet.
- Set `metamta.reply-handler-domain` to `phorge.example.com`
(whatever you configured the MX record for).
That's it! If everything is working properly you should be able to send email
to `anything@phorge.example.com` and it should appear in
`bin/mail list-inbound` within a few seconds.
Local MTA: Installing Mailparse
===============================
If you're going to run your own MTA, you need to install the PECL mailparse
extension. In theory, you can do that with:
$ sudo pecl install mailparse
You may run into an error like "needs mbstring". If so, try:
$ sudo yum install php-mbstring # or equivalent
$ sudo pecl install -n mailparse
If you get a linker error like this:
COUNTEREXAMPLE
PHP Warning: PHP Startup: Unable to load dynamic library
'/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so:
undefined symbol: mbfl_name2no_encoding in Unknown on line 0
...you need to edit your php.ini file so that mbstring.so is loaded **before**
mailparse.so. This is not the default if you have individual files in
`php.d/`.
Local MTA: Configuring Sendmail
===============================
Before you can configure Sendmail, you need to install Mailparse. See the
section "Installing Mailparse" above.
Sendmail is very difficult to configure. First, you need to configure it for
your domain so that mail can be delivered correctly. In broad strokes, this
probably means something like this:
- add an MX record;
- make sendmail listen on external interfaces;
- open up port 25 if necessary (e.g., in your EC2 security policy);
- add your host to /etc/mail/local-host-names; and
- restart sendmail.
Now, you can actually configure sendmail to deliver to Phorge. In
`/etc/aliases`, add an entry like this:
phorge: "| /path/to/phorge/scripts/mail/mail_handler.php"
If you use the `PHABRICATOR_ENV` environmental variable to select a
configuration, you can pass the value to the script as an argument:
.../path/to/mail_handler.php <ENV>
This is an advanced feature which is rarely used. Most installs should run
without an argument.
After making this change, run `sudo newaliases`. Now you likely need to symlink
this script into `/etc/smrsh/`:
sudo ln -s /path/to/phorge/scripts/mail/mail_handler.php /etc/smrsh/
Finally, edit `/etc/mail/virtusertable` and add an entry like this:
@yourdomain.com phorge@localhost
That will forward all mail to @yourdomain.com to the Phorge processing
script. Run `sudo /etc/mail/make` or similar and then restart sendmail with
`sudo /etc/init.d/sendmail restart`.
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index 07cacb6b25..91c8962b59 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,517 +1,517 @@
@title Configuring Outbound Email
@group config
Instructions for configuring Phorge to send email and other types of
messages, like text messages.
Overview
========
Phorge sends outbound messages through "mailers". Most mailers send
email and most messages are email messages, but mailers may also send other
types of messages (like text messages).
Phorge can send outbound messages through multiple different mailers,
including a local mailer or various third-party services. Options include:
| Send Mail With | Setup | Cost | Inbound | Media | Notes |
|----------------|-------|------|---------|-------|-------|
| Postmark | Easy | Cheap | Yes | Email | Recommended |
| Amazon SES | Easy | Cheap | No | Email | |
| SendGrid | Medium | Cheap | Yes | Email | |
| Twilio | Easy | Cheap | No | SMS | Recommended |
| Amazon SNS | Easy | Cheap | No | SMS | Recommended |
| External SMTP | Medium | Varies | No | Email | Gmail, etc. |
| Local SMTP | Hard | Free | No | Email | sendmail, postfix, etc |
| Custom | Hard | Free | No | All | Write a custom mailer. |
| Drop in a Hole | Easy | Free | No | All | Drops mail in a deep, dark hole. |
| Mailgun | Easy | Cheap | Yes | Email | Discouraged |
See below for details on how to select and configure mail delivery for each
mailer.
For email, Postmark is recommended because it makes it easy to set up inbound
and outbound mail and has a good track record in our production services. Other
services will also generally work well, but they may be more difficult to set
up.
For SMS, Twilio or SNS are recommended. They're also your only upstream
options.
If you have some internal mail or messaging service you'd like to use you can
also write a custom mailer, but this requires digging into the code.
Phorge sends mail in the background, so the daemons need to be running for
it to be able to deliver mail. You should receive setup warnings if they are
not. For more information on using daemons, see
@{article:Managing Daemons with phd}.
Outbound "From" and "To" Addresses
==================================
When Phorge sends outbound mail, it must select some "From" address to
send mail from, since mailers require this.
When mail only has "CC" recipients, Phorge generates a dummy "To" address,
since some mailers require this and some users write mail rules that depend
on whether they appear in the "To" or "CC" line.
In both cases, the address should ideally correspond to a valid, deliverable
mailbox that accepts the mail and then simply discards it. If the address is
not valid, some outbound mail will bounce, and users will receive bounces when
they "Reply All" even if the other recipients for the message are valid. In
contrast, if the address is a real user address, that user will receive a lot
of mail they probably don't want.
If you plan to configure //inbound// mail later, you usually don't need to do
anything. Phorge will automatically create a `noreply@` mailbox which
works the right way (accepts and discards all mail it receives) and
automatically use it when generating addresses.
If you don't plan to configure inbound mail, you may need to configure an
address for Phorge to use. You can do this by setting
`metamta.default-address`.
Configuring Mailers
===================
Configure one or more mailers by listing them in the the `cluster.mailers`
configuration option. Most installs only need to configure one mailer, but you
can configure multiple mailers to provide greater availability in the event of
a service disruption.
A valid `cluster.mailers` configuration looks something like this:
```lang=json
[
{
"key": "mycompany-postmark",
"type": "postmark",
"options": {
"domain": "mycompany.com",
"api-key": "..."
}
},
...
]
```
The supported keys for each mailer are:
- `key`: Required string. A unique name for this mailer.
- `type`: Required string. Identifies the type of mailer. See below for
options.
- `priority`: Optional string. Advanced option which controls load balancing
and failover behavior. See below for details.
- `options`: Optional map. Additional options for the mailer type.
- `inbound`: Optional bool. Use `false` to prevent this mailer from being
used to receive inbound mail.
- `outbound`: Optional bool. Use `false` to prevent this mailer from being
used to send outbound mail.
- `media`: Optional list<string>. Some mailers support delivering multiple
types of messages (like Email and SMS). If you want to configure a mailer
to support only a subset of possible message types, list only those message
types. Normally, you do not need to configure this. See below for a list
of media types.
The `type` field can be used to select these mailer services:
- `ses`: Use Amazon SES.
- `sendgrid`: Use SendGrid.
- `postmark`: Use Postmark.
- `twilio`: Use Twilio.
- `sns`: Use Amazon SNS.
- `mailgun`: Use Mailgun.
It also supports these local mailers:
- `sendmail`: Use the local `sendmail` binary.
- `smtp`: Connect directly to an SMTP server.
- `test`: Internal mailer for testing. Does not send mail.
You can also write your own mailer by extending `PhorgeMailAdapter`.
The `media` field supports these values:
- `email`: Configure this mailer for email.
- `sms`: Configure this mailer for SMS.
Once you've selected a mailer, find the corresponding section below for
instructions on configuring it.
Setting Complex Configuration
=============================
Mailers can not be edited from the web UI. If mailers could be edited from
the web UI, it would give an attacker who compromised an administrator account
a lot of power: they could redirect mail to a server they control and then
intercept mail for any other account, including password reset mail.
For more information about locked configuration options, see
@{article:Configuration Guide: Locked and Hidden Configuration}.
Setting `cluster.mailers` from the command line using `bin/config set` can be
tricky because of shell escaping. The easiest way to do it is to use the
`--stdin` flag. First, put your desired configuration in a file like this:
```lang=json, name=mailers.json
[
{
"key": "test-mailer",
"type": "test"
}
]
```
Then set the value like this:
```
phorge/ $ ./bin/config set --stdin cluster.mailers < mailers.json
```
For alternatives and more information on configuration, see
@{article:Configuration User Guide: Advanced Configuration}
Mailer: Postmark
================
| Media | Email
|---------|
| Inbound | Yes
|---------|
Postmark is a third-party email delivery service. You can learn more at
<https://www.postmarkapp.com/>.
To use this mailer, set `type` to `postmark`, then configure these `options`:
- `access-token`: Required string. Your Postmark access token.
- `inbound-addresses`: Optional list<string>. Address ranges which you
will accept inbound Postmark HTTP webook requests from.
The default address list is preconfigured with Postmark's address range, so
you generally will not need to set or adjust it.
The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or
`::ffff:0:0/96` (IPv6). The default ranges are:
```lang=json
[
"50.31.156.6/32",
"50.31.156.77/32",
"18.217.206.57/32",
"3.134.147.250/32"
]
```
The default address ranges were last updated in December 2021, and were
documented at: <https://postmarkapp.com/support/article/800-ips-for-firewalls>
Mailer: Mailgun
===============
| Media | Email
|---------|
| Inbound | Yes
|---------|
Use of Mailgun is discouraged because of concerns that they may not be a
-trustworthy custodian of sensitive data. See <https://phurl.io/u/mailgun> for
-discussion and context.
+trustworthy custodian of sensitive data.
+See <https://secure.phabricator.com/T13669> for discussion and context.
Mailgun is a third-party email delivery service. You can learn more at
<https://www.mailgun.com>. Mailgun is easy to configure and works well.
To use this mailer, set `type` to `mailgun`, then configure these `options`:
- `api-key`: Required string. Your Mailgun API key.
- `domain`: Required string. Your Mailgun domain.
- `api-hostname`: Optional string. Defaults to "api.mailgun.net". If your
account is in another region (like the EU), you may need to specify a
different hostname. Consult the Mailgun documentation.
Mailer: Amazon SES
==================
| Media | Email
|---------|
| Inbound | No
|---------|
Amazon SES is Amazon's cloud email service. You can learn more at
<https://aws.amazon.com/ses/>.
To use this mailer, set `type` to `ses`, then configure these `options`:
- `access-key`: Required string. Your Amazon SES access key.
- `secret-key`: Required string. Your Amazon SES secret key.
- `region`: Required string. Your Amazon SES region, like `us-west-2`.
- `endpoint`: Required string. Your Amazon SES endpoint, like
`email.us-west-2.amazonaws.com`.
NOTE: Amazon SES **requires you to verify your "From" address**. Configure
which "From" address to use by setting `metamta.default-address` in your
config, then follow the Amazon SES verification process to verify it. You
won't be able to send email until you do this!
Mailer: Twilio
==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Twilio is a third-party notification service. You can learn more at
<https://www.twilio.com/>.
To use this mailer, set `type` to `twilio`, then configure these options:
- `account-sid`: Your Twilio Account SID.
- `auth-token`: Your Twilio Auth Token.
- `from-number`: Number to send text messages from, in E.164 format
(like `+15551237890`).
Mailer: Amazon SNS
==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Amazon SNS is Amazon's cloud notification service. You can learn more at
<https://aws.amazon.com/sns/>. Note that this mailer is only able to send
SMS messages, not emails.
To use this mailer, set `type` to `sns`, then configure these options:
- `access-key`: Required string. Your Amazon SNS access key.
- `secret-key`: Required string. Your Amazon SNS secret key.
- `endpoint`: Required string. Your Amazon SNS endpoint.
- `region`: Required string. Your Amazon SNS region.
You can find the correct `region` value for your endpoint in the SNS
documentation.
Mailer: SendGrid
================
| Media | Email
|---------|
| Inbound | Yes
|---------|
SendGrid is a third-party email delivery service. You can learn more at
<https://sendgrid.com/>.
You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, configure Phorge to use an `smtp` mailer.
To use the REST API mailer, set `type` to `sendgrid`, then configure
these `options`:
- `api-key`: Required string. Your SendGrid API key.
Older versions of the SendGrid API used different sets of credentials,
including an "API User". Make sure you're configuring your "API Key".
Mailer: Sendmail
================
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
This requires a `sendmail` binary to be installed on the system. Most MTAs
(e.g., sendmail, qmail, postfix) should install one for you, but your machine
may not have one installed by default. For install instructions, consult the
documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this
document. If you can already send outbound email from the command line or know
how to configure it, this option is straightforward. If you have no idea how to
do any of this, strongly consider using Postmark instead.
To use this mailer, set `type` to `sendmail`, then configure these `options`:
- `message-id`: Optional bool. Set to `false` if Phorge will not be
able to select a custom "Message-ID" header when sending mail via this
mailer. See "Message-ID Headers" below.
Mailer: SMTP
============
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
You can use this adapter to send mail via an external SMTP server, like Gmail.
To use this mailer, set `type` to `smtp`, then configure these `options`:
- `host`: Required string. The hostname of your SMTP server.
- `port`: Optional int. The port to connect to on your SMTP server.
- `user`: Optional string. Username used for authentication.
- `password`: Optional string. Password for authentication.
- `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use
`ssl` for Gmail.
- `message-id`: Optional bool. Set to `false` if Phorge will not be
able to select a custom "Message-ID" header when sending mail via this
mailer. See "Message-ID Headers" below.
Disable Mail
============
| Media | All
|---------|
| Inbound | No
|---------|
To disable mail, just don't configure any mailers. (You can safely ignore the
setup warning reminding you to set up mailers if you don't plan to configure
any.)
Testing and Debugging Outbound Email
====================================
You can use the `bin/mail` utility to test, debug, and examine outbound mail. In
particular:
phorge/ $ ./bin/mail list-outbound # List outbound mail.
phorge/ $ ./bin/mail show-outbound # Show details about messages.
phorge/ $ ./bin/mail send-test # Send test messages.
Run `bin/mail help <command>` for more help on using these commands.
By default, `bin/mail send-test` sends email messages, but you can use
the `--type` flag to send different types of messages.
You can monitor daemons using the Daemon Console (`/daemon/`, or click
**Daemon Console** from the homepage).
Priorities
==========
By default, Phorge will try each mailer in order: it will try the first
mailer first. If that fails (for example, because the service is not available
at the moment) it will try the second mailer, and so on.
If you want to load balance between multiple mailers instead of using one as
a primary, you can set `priority`. Phorge will start with mailers in the
highest priority group and go through them randomly, then fall back to the
next group.
For example, if you have two SMTP servers and you want to balance requests
between them and then fall back to Postmark if both fail, configure priorities
like this:
```lang=json
[
{
"key": "smtp-uswest",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "smtp-useast",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "postmark-fallback",
"type": "postmark",
"options": "..."
}
}
```
Phorge will start with servers in the highest priority group (the group
with the **largest** `priority` number). In this example, the highest group is
`300`, which has the two SMTP servers. They'll be tried in random order first.
If both fail, Phorge will move on to the next priority group. In this
example, there are no other priority groups.
If it still hasn't sent the mail, Phorge will try servers which are not
in any priority group, in the configured order. In this example there is
only one such server, so it will try to send via Postmark.
Message-ID Headers
==================
Email has a "Message-ID" header which is important for threading messages
correctly in mail clients. Normally, Phorge is free to select its own
"Message-ID" header values for mail it sends.
However, some mailers (including Amazon SES) do not allow selection of custom
"Message-ID" values and will ignore or replace the "Message-ID" in mail that
is submitted through them.
When Phorge adds other mail headers which affect threading, like
"In-Reply-To", it needs to know if its "Message-ID" headers will be respected
or not to select header values which will produce good threading behavior. If
we guess wrong and think we can set a "Message-ID" header when we can't, you
may get poor threading behavior in mail clients.
For most mailers (like Postmark, Mailgun, and Amazon SES), the correct setting
will be selected for you automatically, because the behavior of the mailer
is knowable ahead of time. For example, we know Amazon SES will never respect
our "Message-ID" headers.
However, if you're sending mail indirectly through a mailer like SMTP or
Sendmail, the mail might or might not be routing through some mail service
which will ignore or replace the "Message-ID" header.
For example, your local mailer might submit mail to Mailgun (so "Message-ID"
will work), or to Amazon SES (so "Message-ID" will not work), or to some other
mail service (which we may not know anything about). We can't make a reliable
guess about whether "Message-ID" will be respected or not based only on
the local mailer configuration.
By default, we check if the mailer has a hostname we recognize as belonging
to a service which does not allow us to set a "Message-ID" header. If we don't
recognize the hostname (which is very common, since these services are most
often configured against the localhost or some other local machine), we assume
we can set a "Message-ID" header.
If the outbound pathway does not actually allow selection of a "Message-ID"
header, you can set the `message-id` option on the mailer to `false` to tell
Phorge that it should not assume it can select a value for this header.
For example, if you are sending mail via a local Postfix server which then
forwards the mail to Amazon SES (a service which does not allow selection of
a "Message-ID" header), your `smtp` configuration in Phorge should
specify `"message-id": false`.
Next Steps
==========
Continue by:
- @{article:Configuring Inbound Email} so users can reply to email they
receive about revisions and tasks to interact with them; or
- learning about daemons with @{article:Managing Daemons with phd}; or
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/userguide/multi_factor_auth.diviner b/src/docs/user/userguide/multi_factor_auth.diviner
index d445727e6c..9ef92157c9 100644
--- a/src/docs/user/userguide/multi_factor_auth.diviner
+++ b/src/docs/user/userguide/multi_factor_auth.diviner
@@ -1,222 +1,222 @@
@title User Guide: Multi-Factor Authentication
@group userguide
Explains how multi-factor authentication works in Phorge.
Overview
========
Multi-factor authentication allows you to add additional credentials to your
account to make it more secure.
Once multi-factor authentication is configured on your account, you'll usually
use your mobile phone to provide an authorization code or an extra confirmation
when you try to log in to a new session or take certain actions (like changing
your password).
Requiring you to prove you're really you by asking for something you know (your
password) //and// something you have (your mobile phone) makes it much harder
for attackers to access your account. The phone is an additional "factor" which
protects your account from attacks.
How Multi-Factor Authentication Works
=====================================
If you've configured multi-factor authentication and try to log in to your
account or take certain sensitive actions (like changing your password),
you'll be stopped and asked to enter additional credentials.
Usually, this means you'll receive an SMS with a authorization code on your
phone, or you'll open an app on your phone which will show you a authorization
code or ask you to confirm the action. If you're given a authorization code,
you'll enter it into Phorge.
If you're logging in, Phorge will log you in after you enter the code.
If you're taking a sensitive action, Phorge will sometimes put your
account in "high security" mode for a few minutes. In this mode, you can take
sensitive actions like changing passwords or SSH keys freely, without
entering any more credentials.
You can explicitly leave high security once you're done performing account
management, or your account will naturally return to normal security after a
short period of time.
While your account is in high security, you'll see a notification on screen
with instructions for returning to normal security.
Configuring Multi-Factor Authentication
=======================================
To manage authentication factors for your account, go to
{nav Settings > Multi-Factor Auth}. You can use this control panel to add
or remove authentication factors from your account.
You can also rename a factor by clicking the name. This can help you identify
factors if you have several similar factors attached to your account.
For a description of the available factors, see the next few sections.
Factor: Mobile Phone App (TOTP)
===============================
TOTP stands for "Time-based One-Time Password". This factor operates by having
you enter authorization codes from your mobile phone into Phorge. The codes
change every 30 seconds, so you will need to have your phone with you in order
to enter them.
To use this factor, you'll download an application onto your smartphone which
can compute these codes. Two applications which work well are **Authy** and
**Google Authenticator**. These applications are free, and you can find and
download them from the appropriate store on your device.
Your company may have a preferred application, or may use some other
application, so check any in-house documentation for details. In general, any
TOTP application should work properly.
After you've downloaded the application onto your phone, use the Phorge
settings panel to add a factor to your account. You'll be prompted to scan a
QR code, and then read an authorization code from your phone and type it into
Phorge.
Later, when you need to authenticate, you'll follow this same process: launch
the application, read the authorization code, and type it into Phorge.
This will prove you have your phone.
Don't lose your phone! You'll need it to log into Phorge in the future.
Factor: SMS
===========
This factor operates by texting you a short authorization code when you try to
log in or perform a sensitive action.
To use SMS, first add your phone number in {nav Settings > Contact Numbers}.
Once a primary contact number is configured on your account, you'll be able
to add an SMS factor.
To enroll in SMS, you'll be sent a confirmation code to make sure your contact
number is correct and SMS is being delivered properly. Enter it when prompted.
When you're asked to confirm your identity in the future, you'll be texted
an authorization code to enter into the prompt.
(WARNING) SMS is a very weak factor and can be compromised or intercepted. For
-details, see: <https://phurl.io/u/sms>.
+details, see: <https://secure.phabricator.com/T13241>.
Factor: Duo
===========
This factor supports integration with [[ https://duo.com/ | Duo Security ]], a
third-party authentication service popular with enterprises that have a lot of
policies to enforce.
To use Duo, you'll install the Duo application on your phone. When you try
to take a sensitive action, you'll be asked to confirm it in the application.
Administration: Configuration
=============================
New Phorge installs start without any multi-factor providers enabled.
Users won't be able to add new factors until you set up multi-factor
authentication by configuring at least one provider.
Configure new providers in {nav Auth > Multi-Factor}.
Providers may be in these states:
- **Active**: Users may add new factors. Users will be prompted to respond
to challenges from these providers when they take a sensitive action.
- **Deprecated**: Users may not add new factors, but they will still be
asked to respond to challenges from existing factors.
- **Disabled**: Users may not add new factors, and existing factors will
not be used. If MFA is required and a user only has disabled factors,
they will be forced to add a new factor.
If you want to change factor types for your organization, the process will
normally look something like this:
- Configure and test a new provider.
- Deprecate the old provider.
- Notify users that the old provider is deprecated and that they should move
to the new provider at their convenience, but before some upcoming
deadline.
- Once the deadline arrives, disable the old provider.
Administration: Requiring MFA
=============================
As an administrator, you can require all users to add MFA to their accounts by
setting the `security.require-multi-factor-auth` option in Config.
Administration: Recovering from Lost Factors
============================================
If a user has lost a factor associated with their account (for example, their
phone has been lost or damaged), an administrator with host access can strip
the factor off their account so that they can log in without it.
IMPORTANT: Before stripping factors from a user account, be absolutely certain
that the user is who they claim to be!
It is important to verify the user is who they claim they are before stripping
factors because an attacker might pretend to be a user who has lost their phone
in order to bypass multi-factor authentication. It is much easier for a typical
attacker to spoof an email with a sad story in it than it is for a typical
attacker to gain access to a mobile phone.
A good way to verify user identity is to meet them in person and have them
solemnly swear an oath that they lost their phone and are very sorry and
definitely won't do it again. You can also work out a secret handshake in
advance and require them to perform it. But no matter what you do, be certain
the user (not an attacker //pretending// to be the user) is really the one
making the request before stripping factors.
After verifying identity, administrators with host access can strip
authentication factors from user accounts using the `bin/auth strip` command.
For example, to strip all factors from the account of a user who has lost
their phone, run this command:
```lang=console
# Strip all factors from a given user account.
phorge/ $ ./bin/auth strip --user <username> --all-types
```
You can run `bin/auth help strip` for more detail and all available flags and
arguments.
This command can selectively strip factors by factor type. You can use
`bin/auth list-factors` to get a list of available factor types.
```lang=console
# Show supported factor types.
phorge/ $ ./bin/auth list-factors
```
Once you've identified the factor types you want to strip, you can strip
matching factors by using the `--type` flag to specify one or more factor
types:
```lang=console
# Strip all SMS and TOTP factors for a user.
phorge/ $ ./bin/auth strip --user <username> --type sms --type totp
```
The `bin/auth strip` command can also selectively strip factors for certain
providers. This is more granular than stripping all factors of a given type.
You can use `bin/auth list-mfa-providers` to get a list of providers.
Once you have a provider PHID, use `--provider` to select factors to strip:
```lang=console
# Strip all factors for a particular provider.
phorge/ $ ./bin/auth strip --user <username> --provider <providerPHID>
```
diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
index b4a4314ac0..10c6295587 100644
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -1,792 +1,792 @@
<?php
/**
* Handle request startup, before loading the environment or libraries. This
* class bootstraps the request state up to the point where we can enter
* Phabricator code.
*
* NOTE: This class MUST NOT have any dependencies. It runs before libraries
* load.
*
* Rate Limiting
* =============
*
* Phabricator limits the rate at which clients can request pages, and issues
* HTTP 429 "Too Many Requests" responses if clients request too many pages too
* quickly. Although this is not a complete defense against high-volume attacks,
* it can protect an install against aggressive crawlers, security scanners,
* and some types of malicious activity.
*
* To perform rate limiting, each page increments a score counter for the
* requesting user's IP. The page can give the IP more points for an expensive
* request, or fewer for an authetnicated request.
*
* Score counters are kept in buckets, and writes move to a new bucket every
* minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
* the oldest bucket is discarded. This provides a simple mechanism for keeping
* track of scores without needing to store, access, or read very much data.
*
* Users are allowed to accumulate up to 1000 points per minute, averaged across
* all of the tracked buckets.
*
* @task info Accessing Request Information
* @task hook Startup Hooks
* @task apocalypse In Case Of Apocalypse
* @task validation Validation
* @task ratelimit Rate Limiting
* @task phases Startup Phase Timers
* @task request-path Request Path
*/
final class PhabricatorStartup {
private static $startTime;
private static $debugTimeLimit;
private static $accessLog;
private static $capturingOutput;
private static $rawInput;
private static $oldMemoryLimit;
private static $phases;
private static $limits = array();
private static $requestPath;
/* -( Accessing Request Information )-------------------------------------- */
/**
* @task info
*/
public static function getStartTime() {
return self::$startTime;
}
/**
* @task info
*/
public static function getMicrosecondsSinceStart() {
// This is the same as "phutil_microseconds_since()", but we may not have
// loaded libraries yet.
return (int)(1000000 * (microtime(true) - self::getStartTime()));
}
/**
* @task info
*/
public static function setAccessLog($access_log) {
self::$accessLog = $access_log;
}
/**
* @task info
*/
public static function getRawInput() {
if (self::$rawInput === null) {
$stream = new AphrontRequestStream();
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
$encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
$stream->setEncoding($encoding);
}
$input = '';
do {
$bytes = $stream->readData();
if ($bytes === null) {
break;
}
$input .= $bytes;
} while (true);
self::$rawInput = $input;
}
return self::$rawInput;
}
/* -( Startup Hooks )------------------------------------------------------ */
/**
* @param float Request start time, from `microtime(true)`.
* @task hook
*/
public static function didStartup($start_time) {
self::$startTime = $start_time;
self::$phases = array();
self::$accessLog = null;
self::$requestPath = null;
static $registered;
if (!$registered) {
// NOTE: This protects us against multiple calls to didStartup() in the
// same request, but also against repeated requests to the same
// interpreter state, which we may implement in the future.
register_shutdown_function(array(__CLASS__, 'didShutdown'));
$registered = true;
}
self::setupPHP();
self::verifyPHP();
// If we've made it this far, the environment isn't completely broken so
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
self::connectRateLimits();
self::normalizeInput();
self::readRequestPath();
self::beginOutputCapture();
}
/**
* @task hook
*/
public static function didShutdown() {
// Disconnect any active rate limits before we shut down. If we don't do
// this, requests which exit early will lock a slot in any active
// connection limits, and won't count for rate limits.
self::disconnectRateLimits(array());
$event = error_get_last();
if (!$event) {
return;
}
switch ($event['type']) {
case E_ERROR:
case E_PARSE:
case E_COMPILE_ERROR:
break;
default:
return;
}
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
if ($event) {
// Even though we should be emitting this as text-plain, escape things
// just to be sure since we can't really be sure what the program state
// is when we get here.
$msg .= htmlspecialchars(
$event['message']."\n\n".$event['file'].':'.$event['line'],
ENT_QUOTES,
'UTF-8');
}
// flip dem tables
$msg .= "\n\n\n";
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
self::didFatal($msg);
}
public static function loadCoreLibraries() {
$phabricator_root = dirname(dirname(dirname(__FILE__)));
$libraries_root = dirname($phabricator_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set(
'include_path',
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
$ok = @include_once $root.'arcanist/src/init/init-library.php';
if (!$ok) {
self::didFatal(
'Unable to load the "Arcanist" library. Put "arcanist/" next to '.
'"phorge/" on disk.');
}
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($phabricator_root.'/src');
}
/* -( Output Capture )----------------------------------------------------- */
public static function beginOutputCapture() {
if (self::$capturingOutput) {
self::didFatal('Already capturing output!');
}
self::$capturingOutput = true;
ob_start();
}
public static function endOutputCapture() {
if (!self::$capturingOutput) {
return null;
}
self::$capturingOutput = false;
return ob_get_clean();
}
/* -( Debug Time Limit )--------------------------------------------------- */
/**
* Set a time limit (in seconds) for the current script. After time expires,
* the script fatals.
*
* This works like `max_execution_time`, but prints out a useful stack trace
* when the time limit expires. This is primarily intended to make it easier
* to debug pages which hang by allowing extraction of a stack trace: set a
* short debug limit, then use the trace to figure out what's happening.
*
* The limit is implemented with a tick function, so enabling it implies
* some accounting overhead.
*
* @param int Time limit in seconds.
* @return void
*/
public static function setDebugTimeLimit($limit) {
self::$debugTimeLimit = $limit;
static $initialized = false;
if (!$initialized) {
declare(ticks=1);
register_tick_function(array(__CLASS__, 'onDebugTick'));
$initialized = true;
}
}
/**
* Callback tick function used by @{method:setDebugTimeLimit}.
*
* Fatals with a useful stack trace after the time limit expires.
*
* @return void
*/
public static function onDebugTick() {
$limit = self::$debugTimeLimit;
if (!$limit) {
return;
}
$elapsed = (microtime(true) - self::getStartTime());
if ($elapsed > $limit) {
$frames = array();
foreach (debug_backtrace() as $frame) {
$file = isset($frame['file']) ? $frame['file'] : '-';
$file = basename($file);
$line = isset($frame['line']) ? $frame['line'] : '-';
$class = isset($frame['class']) ? $frame['class'].'->' : null;
$func = isset($frame['function']) ? $frame['function'].'()' : '?';
$frames[] = "{$file}:{$line} {$class}{$func}";
}
self::didFatal(
"Request aborted by debug time limit after {$limit} seconds.\n\n".
"STACK TRACE\n".
implode("\n", $frames));
}
}
/* -( In Case of Apocalypse )---------------------------------------------- */
/**
* Fatal the request completely in response to an exception, sending a plain
* text message to the client. Calls @{method:didFatal} internally.
*
* @param string Brief description of the exception context, like
* `"Rendering Exception"`.
* @param Throwable The exception itself.
* @param bool True if it's okay to show the exception's stack trace
* to the user. The trace will always be logged.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didEncounterFatalException(
$note,
$ex,
$show_trace) {
$message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
$full_message = $message;
$full_message .= "\n\n";
$full_message .= $ex->getTraceAsString();
if ($show_trace) {
$message = $full_message;
}
self::didFatal($message, $full_message);
}
/**
* Fatal the request completely, sending a plain text message to the client.
*
* @param string Plain text message to send to the client.
* @param string Plain text message to send to the error log. If not
* provided, the client message is used. You can pass a more
* detailed message here (e.g., with stack traces) to avoid
* showing it to users.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didFatal($message, $log_message = null) {
if ($log_message === null) {
$log_message = $message;
}
self::endOutputCapture();
$access_log = self::$accessLog;
if ($access_log) {
// We may end up here before the access log is initialized, e.g. from
// verifyPHP().
$access_log->setData(
array(
'c' => 500,
));
$access_log->write();
}
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 500);
error_log($log_message);
echo $message."\n";
exit(1);
}
/* -( Validation )--------------------------------------------------------- */
/**
* @task validation
*/
private static function setupPHP() {
error_reporting(E_ALL | E_STRICT);
self::$oldMemoryLimit = ini_get('memory_limit');
ini_set('memory_limit', -1);
// If we have libxml, disable the incredibly dangerous entity loader.
// PHP 8 deprecates this function and disables this by default; remove once
// PHP 7 is no longer supported or a future version has removed the function
// entirely.
if (function_exists('libxml_disable_entity_loader')) {
@libxml_disable_entity_loader(true);
}
// See T13060. If the locale for this process (the parent process) is not
// a UTF-8 locale we can encounter problems when launching subprocesses
// which receive UTF-8 parameters in their command line argument list.
@setlocale(LC_ALL, 'en_US.UTF-8');
$config_map = array(
// See PHI1894. Keep "args" in exception backtraces.
'zend.exception_ignore_args' => 0,
// See T13100. We'd like the regex engine to fail, rather than segfault,
// if handed a pathological regular expression.
'pcre.backtrack_limit' => 10000,
'pcre.recusion_limit' => 10000,
// NOTE: Arcanist applies a similar set of startup options for CLI
// environments in "init-script.php". Changes here may also be
// appropriate to apply there.
);
foreach ($config_map as $config_key => $config_value) {
ini_set($config_key, $config_value);
}
}
/**
* @task validation
*/
public static function getOldMemoryLimit() {
return self::$oldMemoryLimit;
}
/**
* @task validation
*/
private static function normalizeInput() {
// Replace superglobals with unfiltered versions, disrespect php.ini (we
// filter ourselves).
// NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
// changes made in "preamble.php".
// NOTE: WE don't filter INPUT_POST because we may be constructing it
// lazily if "enable_post_data_reading" is disabled.
$filter = array(
INPUT_GET,
INPUT_ENV,
INPUT_COOKIE,
);
foreach ($filter as $type) {
$filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
if (!is_array($filtered)) {
continue;
}
switch ($type) {
case INPUT_GET:
$_GET = array_merge($_GET, $filtered);
break;
case INPUT_COOKIE:
$_COOKIE = array_merge($_COOKIE, $filtered);
break;
case INPUT_ENV;
$env = array_merge($_ENV, $filtered);
$_ENV = self::filterEnvSuperglobal($env);
break;
}
}
self::rebuildRequest();
}
/**
* @task validation
*/
public static function rebuildRequest() {
// Rebuild $_REQUEST, respecting order declared in ".ini" files.
$order = ini_get('request_order');
if (!$order) {
$order = ini_get('variables_order');
}
if (!$order) {
// $_REQUEST will be empty, so leave it alone.
return;
}
$_REQUEST = array();
for ($ii = 0; $ii < strlen($order); $ii++) {
switch ($order[$ii]) {
case 'G':
$_REQUEST = array_merge($_REQUEST, $_GET);
break;
case 'P':
$_REQUEST = array_merge($_REQUEST, $_POST);
break;
case 'C':
$_REQUEST = array_merge($_REQUEST, $_COOKIE);
break;
default:
// $_ENV and $_SERVER never go into $_REQUEST.
break;
}
}
}
/**
* Adjust `$_ENV` before execution.
*
* Adjustments here primarily impact the environment as seen by subprocesses.
* The environment is forwarded explicitly by @{class:ExecFuture}.
*
* @param map<string, wild> Input `$_ENV`.
* @return map<string, string> Suitable `$_ENV`.
* @task validation
*/
private static function filterEnvSuperglobal(array $env) {
// In some configurations, we may get "argc" and "argv" set in $_ENV.
// These are not real environmental variables, and "argv" may have an array
// value which can not be forwarded to subprocesses. Remove these from the
// environment if they are present.
unset($env['argc']);
unset($env['argv']);
return $env;
}
/**
* @task validation
*/
private static function verifyPHP() {
$required_version = '5.2.3';
if (version_compare(PHP_VERSION, $required_version) < 0) {
self::didFatal(
"You are running PHP version '".PHP_VERSION."', which is older than ".
"the minimum version, '{$required_version}'. Update to at least ".
"'{$required_version}'.");
}
if (function_exists('get_magic_quotes_gpc')) {
if (@get_magic_quotes_gpc()) {
self::didFatal(
'Your server is configured with the PHP language feature '.
'"magic_quotes_gpc" enabled.'.
"\n\n".
'This feature is "highly discouraged" by PHP\'s developers, and '.
'has been removed entirely in PHP8.'.
"\n\n".
'You must disable "magic_quotes_gpc" to run Phabricator. Consult '.
'the PHP manual for instructions.');
}
}
if (extension_loaded('apc')) {
$apc_version = phpversion('apc');
$known_bad = array(
'3.1.14' => true,
'3.1.15' => true,
'3.1.15-dev' => true,
);
if (isset($known_bad[$apc_version])) {
self::didFatal(
"You have APC {$apc_version} installed. This version of APC is ".
"known to be bad, and does not work with Phabricator (it will ".
"cause Phabricator to fatal unrecoverably with nonsense errors). ".
"Downgrade to version 3.1.13.");
}
}
if (isset($_SERVER['HTTP_PROXY'])) {
self::didFatal(
'This HTTP request included a "Proxy:" header, poisoning the '.
'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
- 'request. For details, see: https://phurl.io/u/httpoxy');
+ 'request. For details, see: https://secure.phabricator.com/T11359');
}
}
/**
* @task request-path
*/
private static function readRequestPath() {
// See T13575. The request path may be provided in:
//
// - the "$_GET" parameter "__path__" (normal for Apache and nginx); or
// - the "$_SERVER" parameter "REQUEST_URI" (normal for the PHP builtin
// webserver).
//
// Locate it wherever it is, and store it for later use. Note that writing
// to "$_REQUEST" here won't always work, because later code may rebuild
// "$_REQUEST" from other sources.
if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
self::setRequestPath($_REQUEST['__path__']);
return;
}
// Compatibility with PHP 5.4+ built-in web server.
if (php_sapi_name() == 'cli-server') {
$path = parse_url($_SERVER['REQUEST_URI']);
self::setRequestPath($path['path']);
return;
}
if (!isset($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is not set. Your rewrite rules ".
"are not configured correctly.");
}
if (!strlen($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is set, but empty. Your rewrite rules ".
"are not configured correctly. The '__path__' should always ".
"begin with a '/'.");
}
}
/**
* @task request-path
*/
public static function getRequestPath() {
$path = self::$requestPath;
if ($path === null) {
self::didFatal(
'Request attempted to access request path, but no request path is '.
'available for this request. You may be calling web request code '.
'from a non-request context, or your webserver may not be passing '.
'a request path to Phabricator in a format that it understands.');
}
return $path;
}
/**
* @task request-path
*/
public static function setRequestPath($path) {
self::$requestPath = $path;
}
/* -( Rate Limiting )------------------------------------------------------ */
/**
* Add a new client limits.
*
* @param PhabricatorClientLimit New limit.
* @return PhabricatorClientLimit The limit.
*/
public static function addRateLimit(PhabricatorClientLimit $limit) {
self::$limits[] = $limit;
return $limit;
}
/**
* Apply configured rate limits.
*
* If any limit is exceeded, this method terminates the request.
*
* @return void
* @task ratelimit
*/
private static function connectRateLimits() {
$limits = self::$limits;
$reason = null;
$connected = array();
foreach ($limits as $limit) {
$reason = $limit->didConnect();
$connected[] = $limit;
if ($reason !== null) {
break;
}
}
// If we're killing the request here, disconnect any limits that we
// connected to try to keep the accounting straight.
if ($reason !== null) {
foreach ($connected as $limit) {
$limit->didDisconnect(array());
}
self::didRateLimit($reason);
}
}
/**
* Tear down rate limiting and allow limits to score the request.
*
* @param map<string, wild> Additional, freeform request state.
* @return void
* @task ratelimit
*/
public static function disconnectRateLimits(array $request_state) {
$limits = self::$limits;
// Remove all limits before disconnecting them so this works properly if
// it runs twice. (We run this automatically as a shutdown handler.)
self::$limits = array();
foreach ($limits as $limit) {
$limit->didDisconnect($request_state);
}
}
/**
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user
* has exceeded application rate limits) and exit.
*
* @return exit This method **does not return**.
* @task ratelimit
*/
private static function didRateLimit($reason) {
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 429);
echo $reason;
exit(1);
}
/* -( Startup Timers )----------------------------------------------------- */
/**
* Record the beginning of a new startup phase.
*
* For phases which occur before @{class:PhabricatorStartup} loads, save the
* time and record it with @{method:recordStartupPhase} after the class is
* available.
*
* @param string Phase name.
* @task phases
*/
public static function beginStartupPhase($phase) {
self::recordStartupPhase($phase, microtime(true));
}
/**
* Record the start time of a previously executed startup phase.
*
* For startup phases which occur after @{class:PhabricatorStartup} loads,
* use @{method:beginStartupPhase} instead. This method can be used to
* record a time before the class loads, then hand it over once the class
* becomes available.
*
* @param string Phase name.
* @param float Phase start time, from `microtime(true)`.
* @task phases
*/
public static function recordStartupPhase($phase, $time) {
self::$phases[$phase] = $time;
}
/**
* Get information about startup phase timings.
*
* Sometimes, performance problems can occur before we start the profiler.
* Since the profiler can't examine these phases, it isn't useful in
* understanding their performance costs.
*
* Instead, the startup process marks when it enters various phases using
* @{method:beginStartupPhase}. A later call to this method can retrieve this
* information, which can be examined to gain greater insight into where
* time was spent. The output is still crude, but better than nothing.
*
* @task phases
*/
public static function getPhases() {
return self::$phases;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Apr 29, 12:55 PM (1 d, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
108328
Default Alt Text
(106 KB)

Event Timeline