Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php
index 59aac4c5ef..a423569421 100644
--- a/src/applications/almanac/servicetype/AlmanacServiceType.php
+++ b/src/applications/almanac/servicetype/AlmanacServiceType.php
@@ -1,76 +1,76 @@
<?php
abstract class AlmanacServiceType extends Phobject {
/**
* Return a very short human-readable name for this service type, like
* "Custom".
*
* @return string Very short human-readable service type name.
*/
abstract public function getServiceTypeShortName();
/**
* Return a short, human-readable name for this service type, like
* "Custom Service".
*
* @return string Human-readable name for this service type.
*/
abstract public function getServiceTypeName();
/**
* Return a brief summary of this service type.
*
* This summary should be a sentence or two long.
*
* @return string Brief, human-readable description of this service type.
*/
abstract public function getServiceTypeDescription();
public function getServiceTypeIcon() {
return 'fa-cog';
}
/**
* Return `true` if this service type is a Phabricator cluster service type.
*
* These special services change the behavior of Phabricator, and require
* elevated permission to create.
*
* @return bool True if this is a Phabricator cluster service type.
*/
public function isClusterServiceType() {
return false;
}
public function getDefaultPropertyMap() {
return array();
}
public function getFieldSpecifications() {
return array();
}
public function getStatusMessages(AlmanacService $service) {
return array();
}
/**
* List all available service type implementations.
*
* @return map<string, object> Dictionary of available service types.
*/
public static function getAllServiceTypes() {
- $types = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- return msort($types, 'getServiceTypeName');
+ ->setUniqueMethod('getServiceTypeShortName')
+ ->setSortMethod('getServiceTypeName')
+ ->execute();
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index f579667ce8..7cddc2758c 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,74 +1,49 @@
<?php
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
abstract public function getFactorKey();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
abstract public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
$validation_result);
abstract public function processValidateFactorForm(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request);
public function isFactorValid(
PhabricatorAuthFactorConfig $config,
$validation_result) {
return (idx($validation_result, 'valid') === true);
}
public function getParameterName(
PhabricatorAuthFactorConfig $config,
$name) {
return 'authfactor.'.$config->getID().'.'.$name;
}
public static function getAllFactors() {
- static $factors;
-
- if ($factors === null) {
- $map = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $factors = array();
- foreach ($map as $factor) {
- $key = $factor->getFactorKey();
- if (empty($factors[$key])) {
- $factors[$key] = $factor;
- } else {
- $this_class = get_class($factor);
- $that_class = get_class($factors[$key]);
-
- throw new Exception(
- pht(
- 'Two auth factors (with classes "%s" and "%s") both provide '.
- 'implementations with the same key ("%s"). Each factor must '.
- 'have a unique key.',
- $this_class,
- $that_class,
- $key));
- }
- }
- }
-
- return $factors;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getFactorKey')
+ ->execute();
}
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
->setFactorKey($this->getFactorKey());
}
}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
index 0b03bed7ff..594bb89ea6 100644
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -1,501 +1,494 @@
<?php
abstract class PhabricatorAuthProvider extends Phobject {
private $providerConfig;
public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function hasProviderConfig() {
return (bool)$this->providerConfig;
}
public function getProviderConfig() {
if ($this->providerConfig === null) {
throw new PhutilInvalidStateException('attachProviderConfig');
}
return $this->providerConfig;
}
public function getConfigurationHelp() {
return null;
}
public function getDefaultProviderConfig() {
return id(new PhabricatorAuthProviderConfig())
->setProviderClass(get_class($this))
->setIsEnabled(1)
->setShouldAllowLogin(1)
->setShouldAllowRegistration(1)
->setShouldAllowLink(1)
->setShouldAllowUnlink(1);
}
public function getNameForCreate() {
return $this->getProviderName();
}
public function getDescriptionForCreate() {
return null;
}
public function getProviderKey() {
return $this->getAdapter()->getAdapterKey();
}
public function getProviderType() {
return $this->getAdapter()->getAdapterType();
}
public function getProviderDomain() {
return $this->getAdapter()->getAdapterDomain();
}
public static function getAllBaseProviders() {
- static $providers;
-
- if ($providers === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
- $providers = $objects;
- }
-
- return $providers;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->execute();
}
public static function getAllProviders() {
static $providers;
if ($providers === null) {
$objects = self::getAllBaseProviders();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$providers = array();
foreach ($configs as $config) {
if (!isset($objects[$config->getProviderClass()])) {
// This configuration is for a provider which is not installed.
continue;
}
$object = clone $objects[$config->getProviderClass()];
$object->attachProviderConfig($config);
$key = $object->getProviderKey();
if (isset($providers[$key])) {
throw new Exception(
pht(
"Two authentication providers use the same provider key ".
"('%s'). Each provider must be identified by a unique key.",
$key));
}
$providers[$key] = $object;
}
}
return $providers;
}
public static function getAllEnabledProviders() {
$providers = self::getAllProviders();
foreach ($providers as $key => $provider) {
if (!$provider->isEnabled()) {
unset($providers[$key]);
}
}
return $providers;
}
public static function getEnabledProviderByKey($provider_key) {
return idx(self::getAllEnabledProviders(), $provider_key);
}
abstract public function getProviderName();
abstract public function getAdapter();
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
public function shouldAllowLogin() {
return $this->getProviderConfig()->getShouldAllowLogin();
}
public function shouldAllowRegistration() {
return $this->getProviderConfig()->getShouldAllowRegistration();
}
public function shouldAllowAccountLink() {
return $this->getProviderConfig()->getShouldAllowLink();
}
public function shouldAllowAccountUnlink() {
return $this->getProviderConfig()->getShouldAllowUnlink();
}
public function shouldTrustEmails() {
return $this->shouldAllowEmailTrustConfiguration() &&
$this->getProviderConfig()->getShouldTrustEmails();
}
/**
* Should we allow the adapter to be marked as "trusted". This is true for
* all adapters except those that allow the user to type in emails (see
* @{class:PhabricatorPasswordAuthProvider}).
*/
public function shouldAllowEmailTrustConfiguration() {
return true;
}
public function buildLoginForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
}
public function buildInviteForm(PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
}
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
public function buildLinkForm(PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
}
public function shouldAllowAccountRefresh() {
return true;
}
public function buildRefreshForm(
PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
throw new PhutilMethodNotImplementedException();
}
public function createProviders() {
return array($this);
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
return;
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
return;
}
protected function loadOrCreateAccount($account_id) {
if (!strlen($account_id)) {
throw new Exception(pht('Empty account ID!'));
}
$adapter = $this->getAdapter();
$adapter_class = get_class($adapter);
if (!strlen($adapter->getAdapterType())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter type.",
$adapter_class));
}
if (!strlen($adapter->getAdapterDomain())) {
throw new Exception(
pht(
"AuthAdapter (of class '%s') has an invalid implementation: ".
"no adapter domain.",
$adapter_class));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s',
$adapter->getAdapterType(),
$adapter->getAdapterDomain(),
$account_id);
if (!$account) {
$account = id(new PhabricatorExternalAccount())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain())
->setAccountID($account_id);
}
$account->setUsername($adapter->getAccountName());
$account->setRealName($adapter->getAccountRealName());
$account->setEmail($adapter->getAccountEmail());
$account->setAccountURI($adapter->getAccountURI());
$account->setProfileImagePHID(null);
$image_uri = $adapter->getAccountImageURI();
if ($image_uri) {
try {
$name = PhabricatorSlug::normalize($this->getProviderName());
$name = $name.'-profile.jpg';
// TODO: If the image has not changed, we do not need to make a new
// file entry for it, but there's no convenient way to do this with
// PhabricatorFile right now. The storage will get shared, so the impact
// here is negligible.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$image_file = PhabricatorFile::newFromFileDownload(
$image_uri,
array(
'name' => $name,
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
if ($image_file->isViewableImage()) {
$image_file
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setCanCDN(true)
->save();
$account->setProfileImagePHID($image_file->getPHID());
} else {
$image_file->delete();
}
unset($unguarded);
} catch (Exception $ex) {
// Log this but proceed, it's not especially important that we
// be able to pull profile images.
phlog($ex);
}
}
$this->willSaveAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account;
}
public function getLoginURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
}
public function getSettingsURI() {
return '/settings/panel/external/';
}
public function getStartURI() {
$app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
$uri = $app->getApplicationURI('/start/');
return $uri;
}
public function isDefaultRegistrationProvider() {
return false;
}
public function shouldRequireRegistrationPassword() {
return false;
}
public function getDefaultExternalAccount() {
throw new PhutilMethodNotImplementedException();
}
public function getLoginOrder() {
return '500-'.$this->getProviderName();
}
protected function getLoginIcon() {
return 'Generic';
}
public function isLoginFormAButton() {
return false;
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
return null;
}
public function readFormValuesFromProvider() {
return array();
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array();
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
return;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
$account_view = id(new PhabricatorAuthAccountView())
->setExternalAccount($account)
->setAuthProvider($this);
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'mmr mml mst mmb',
),
$account_view));
}
/**
* Return true to use a two-step configuration (setup, configure) instead of
* the default single-step configuration. In practice, this means that
* creating a new provider instance will redirect back to the edit page
* instead of the provider list.
*
* @return bool True if this provider uses two-step configuration.
*/
public function hasSetupStep() {
return false;
}
/**
* Render a standard login/register button element.
*
* The `$attributes` parameter takes these keys:
*
* - `uri`: URI the button should take the user to when clicked.
* - `method`: Optional HTTP method the button should use, defaults to GET.
*
* @param AphrontRequest HTTP request.
* @param string Request mode string.
* @param map Additional parameters, see above.
* @return wild Login button.
*/
protected function renderStandardLoginButton(
AphrontRequest $request,
$mode,
array $attributes = array()) {
PhutilTypeSpec::checkMap(
$attributes,
array(
'method' => 'optional string',
'uri' => 'string',
'sigil' => 'optional string',
));
$viewer = $request->getUser();
$adapter = $this->getAdapter();
if ($mode == 'link') {
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
} else if ($mode == 'invite') {
$button_text = pht('Register Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Login or Register');
} else {
$button_text = pht('Login');
}
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($button_text)
->setSubtext($this->getProviderName());
$uri = $attributes['uri'];
$uri = new PhutilURI($uri);
$params = $uri->getQueryParams();
$uri->setQueryParams(array());
$content = array($button);
foreach ($params as $key => $value) {
$content[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
return phabricator_form(
$viewer,
array(
'method' => idx($attributes, 'method', 'GET'),
'action' => (string)$uri,
'sigil' => idx($attributes, 'sigil'),
),
$content);
}
public function renderConfigurationFooter() {
return null;
}
public function getAuthCSRFCode(AphrontRequest $request) {
$phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
if (!strlen($phcid)) {
throw new Exception(
pht(
'Your browser did not submit a "%s" cookie with client state '.
'information in the request. Check that cookies are enabled. '.
'If this problem persists, you may need to clear your cookies.',
PhabricatorCookies::COOKIE_CLIENTID));
}
return PhabricatorHash::digest($phcid);
}
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
$expect = $this->getAuthCSRFCode($request);
if (!strlen($actual)) {
throw new Exception(
pht(
'The authentication provider did not return a client state '.
'parameter in its response, but one was expected. If this '.
'problem persists, you may need to clear your cookies.'));
}
if ($actual !== $expect) {
throw new Exception(
pht(
'The authentication provider did not return the correct client '.
'state parameter in its response. If this problem persists, you may '.
'need to clear your cookies.'));
}
}
}
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 557594e3b6..1292282d25 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,634 +1,634 @@
<?php
/**
* @task info Application Information
* @task ui UI Integration
* @task uri URI Routing
* @task mail Email integration
* @task fact Fact Integration
* @task meta Application Management
*/
abstract class PhabricatorApplication
extends Phobject
implements PhabricatorPolicyInterface {
const MAX_STATUS_ITEMS = 100;
const GROUP_CORE = 'core';
const GROUP_UTILITIES = 'util';
const GROUP_ADMIN = 'admin';
const GROUP_DEVELOPER = 'developer';
final public static function getApplicationGroups() {
return array(
self::GROUP_CORE => pht('Core Applications'),
self::GROUP_UTILITIES => pht('Utilities'),
self::GROUP_ADMIN => pht('Administration'),
self::GROUP_DEVELOPER => pht('Developer Tools'),
);
}
/* -( Application Information )-------------------------------------------- */
abstract public function getName();
public function getShortDescription() {
return pht('%s Application', $this->getName());
}
final public function isInstalled() {
if (!$this->canUninstall()) {
return true;
}
$prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
if (!$prototypes && $this->isPrototype()) {
return false;
}
$uninstalled = PhabricatorEnv::getEnvConfig(
'phabricator.uninstalled-applications');
return empty($uninstalled[get_class($this)]);
}
public function isPrototype() {
return false;
}
/**
* Return `true` if this application should never appear in application lists
* in the UI. Primarily intended for unit test applications or other
* pseudo-applications.
*
* Few applications should be unlisted. For most applications, use
* @{method:isLaunchable} to hide them from main launch views instead.
*
* @return bool True to remove application from UI lists.
*/
public function isUnlisted() {
return false;
}
/**
* Return `true` if this application is a normal application with a base
* URI and a web interface.
*
* Launchable applications can be pinned to the home page, and show up in the
* "Launcher" view of the Applications application. Making an application
* unlauncahble prevents pinning and hides it from this view.
*
* Usually, an application should be marked unlaunchable if:
*
* - it is available on every page anyway (like search); or
* - it does not have a web interface (like subscriptions); or
* - it is still pre-release and being intentionally buried.
*
* To hide applications more completely, use @{method:isUnlisted}.
*
* @return bool True if the application is launchable.
*/
public function isLaunchable() {
return true;
}
/**
* Return `true` if this application should be pinned by default.
*
* Users who have not yet set preferences see a default list of applications.
*
* @param PhabricatorUser User viewing the pinned application list.
* @return bool True if this application should be pinned by default.
*/
public function isPinnedByDefault(PhabricatorUser $viewer) {
return false;
}
/**
* Returns true if an application is first-party (developed by Phacility)
* and false otherwise.
*
* @return bool True if this application is developed by Phacility.
*/
final public function isFirstParty() {
$where = id(new ReflectionClass($this))->getFileName();
$root = phutil_get_library_root('phabricator');
if (!Filesystem::isDescendant($where, $root)) {
return false;
}
if (Filesystem::isDescendant($where, $root.'/extensions')) {
return false;
}
return true;
}
public function canUninstall() {
return true;
}
final public function getPHID() {
return 'PHID-APPS-'.get_class($this);
}
public function getTypeaheadURI() {
return $this->isLaunchable() ? $this->getBaseURI() : null;
}
public function getBaseURI() {
return null;
}
final public function getApplicationURI($path = '') {
return $this->getBaseURI().ltrim($path, '/');
}
public function getIconURI() {
return null;
}
public function getFontIcon() {
return 'fa-puzzle-piece';
}
public function getApplicationOrder() {
return PHP_INT_MAX;
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getTitleGlyph() {
return null;
}
final public function getHelpMenuItems(PhabricatorUser $viewer) {
$items = array();
$articles = $this->getHelpDocumentationArticles($viewer);
if ($articles) {
$items[] = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName(pht('%s Documentation', $this->getName()));
foreach ($articles as $article) {
$item = id(new PHUIListItemView())
->setName($article['name'])
->setIcon('fa-book')
->setHref($article['href']);
$items[] = $item;
}
}
$command_specs = $this->getMailCommandObjects();
if ($command_specs) {
$items[] = id(new PHUIListItemView())
->setType(PHUIListItemView::TYPE_LABEL)
->setName(pht('Email Help'));
foreach ($command_specs as $key => $spec) {
$object = $spec['object'];
$class = get_class($this);
$href = '/applications/mailcommands/'.$class.'/'.$key.'/';
$item = id(new PHUIListItemView())
->setName($spec['name'])
->setIcon('fa-envelope-o')
->setHref($href);
$items[] = $item;
}
}
return $items;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array();
}
public function getOverview() {
return null;
}
public function getEventListeners() {
return array();
}
public function getRemarkupRules() {
return array();
}
public function getQuicksandURIPatternBlacklist() {
return array();
}
public function getMailCommandObjects() {
return array();
}
/* -( URI Routing )-------------------------------------------------------- */
public function getRoutes() {
return array();
}
/* -( Email Integration )-------------------------------------------------- */
public function supportsEmailIntegration() {
return false;
}
final protected function getInboundEmailSupportLink() {
return PhabricatorEnv::getDocLink('Configuring Inbound Email');
}
public function getAppEmailBlurb() {
throw new PhutilMethodNotImplementedException();
}
/* -( Fact Integration )--------------------------------------------------- */
public function getFactObjectsForAnalysis() {
return array();
}
/* -( UI Integration )----------------------------------------------------- */
/**
* Render status elements (like "3 Waiting Reviews") for application list
* views. These provide a way to alert users to new or pending action items
* in applications.
*
* @param PhabricatorUser Viewing user.
* @return list<PhabricatorApplicationStatusView> Application status elements.
* @task ui
*/
public function loadStatus(PhabricatorUser $user) {
return array();
}
/**
* @return string
* @task ui
*/
final public static function formatStatusCount(
$count,
$limit_string = '%s',
$base_string = '%d') {
if ($count == self::MAX_STATUS_ITEMS) {
$count_str = pht($limit_string, ($count - 1).'+');
} else {
$count_str = pht($base_string, $count);
}
return $count_str;
}
/**
* You can provide an optional piece of flavor text for the application. This
* is currently rendered in application launch views if the application has no
* status elements.
*
* @return string|null Flavor text.
* @task ui
*/
public function getFlavorText() {
return null;
}
/**
* Build items for the main menu.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return list<PHUIListItemView> List of menu items.
* @task ui
*/
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
return array();
}
/**
* Build extra items for the main menu. Generally, this is used to render
* static dropdowns.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return view List of menu items.
* @task ui
*/
public function buildMainMenuExtraNodes(
PhabricatorUser $viewer,
PhabricatorController $controller = null) {
return array();
}
/**
* Build items for the "quick create" menu.
*
* @param PhabricatorUser The viewing user.
* @return list<PHUIListItemView> List of menu items.
*/
public function getQuickCreateItems(PhabricatorUser $viewer) {
return array();
}
/* -( Application Management )--------------------------------------------- */
final public static function getByClass($class_name) {
$selected = null;
$applications = self::getAllApplications();
foreach ($applications as $application) {
if (get_class($application) == $class_name) {
$selected = $application;
break;
}
}
if (!$selected) {
throw new Exception(pht("No application '%s'!", $class_name));
}
return $selected;
}
final public static function getAllApplications() {
static $applications;
if ($applications === null) {
- $apps = id(new PhutilSymbolLoader())
+ $apps = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
+ ->setSortMethod('getApplicationOrder')
+ ->execute();
// Reorder the applications into "application order". Notably, this
// ensures their event handlers register in application order.
- $apps = msort($apps, 'getApplicationOrder');
$apps = mgroup($apps, 'getApplicationGroup');
$group_order = array_keys(self::getApplicationGroups());
$apps = array_select_keys($apps, $group_order) + $apps;
$apps = array_mergev($apps);
$applications = $apps;
}
return $applications;
}
final public static function getAllInstalledApplications() {
$all_applications = self::getAllApplications();
$apps = array();
foreach ($all_applications as $app) {
if (!$app->isInstalled()) {
continue;
}
$apps[] = $app;
}
return $apps;
}
/**
* Determine if an application is installed, by application class name.
*
* To check if an application is installed //and// available to a particular
* viewer, user @{method:isClassInstalledForViewer}.
*
* @param string Application class name.
* @return bool True if the class is installed.
* @task meta
*/
final public static function isClassInstalled($class) {
return self::getByClass($class)->isInstalled();
}
/**
* Determine if an application is installed and available to a viewer, by
* application class name.
*
* To check if an application is installed at all, use
* @{method:isClassInstalled}.
*
* @param string Application class name.
* @param PhabricatorUser Viewing user.
* @return bool True if the class is installed for the viewer.
* @task meta
*/
final public static function isClassInstalledForViewer(
$class,
PhabricatorUser $viewer) {
if ($viewer->isOmnipotent()) {
return true;
}
$cache = PhabricatorCaches::getRequestCache();
$viewer_phid = $viewer->getPHID();
$key = 'app.'.$class.'.installed.'.$viewer_phid;
$result = $cache->getKey($key);
if ($result === null) {
if (!self::isClassInstalled($class)) {
$result = false;
} else {
$result = PhabricatorPolicyFilter::hasCapability(
$viewer,
self::getByClass($class),
PhabricatorPolicyCapability::CAN_VIEW);
}
$cache->setKey($key, $result);
}
return $result;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array_merge(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
),
array_keys($this->getCustomCapabilities()));
}
public function getPolicy($capability) {
$default = $this->getCustomPolicySetting($capability);
if ($default) {
return $default;
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/* -( Policies )----------------------------------------------------------- */
protected function getCustomCapabilities() {
return array();
}
final private function getCustomPolicySetting($capability) {
if (!$this->isCapabilityEditable($capability)) {
return null;
}
$policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
if (isset($policy_locked[$capability])) {
return $policy_locked[$capability];
}
$config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
$app = idx($config, $this->getPHID());
if (!$app) {
return null;
}
$policy = idx($app, 'policy');
if (!$policy) {
return null;
}
return idx($policy, $capability);
}
final private function getCustomCapabilitySpecification($capability) {
$custom = $this->getCustomCapabilities();
if (!isset($custom[$capability])) {
throw new Exception(pht("Unknown capability '%s'!", $capability));
}
return $custom[$capability];
}
final public function getCapabilityLabel($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Can Use Application');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Can Configure Application');
}
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
return $capobj->getCapabilityName();
}
return null;
}
final public function isCapabilityEditable($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->canUninstall();
case PhabricatorPolicyCapability::CAN_EDIT:
return false;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'edit', true);
}
}
final public function getCapabilityCaption($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->canUninstall()) {
return pht(
'This application is required for Phabricator to operate, so all '.
'users must have access to it.');
} else {
return null;
}
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'caption');
}
}
final public function getCapabilityTemplatePHIDType($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
}
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'template');
}
final public function getDefaultObjectTypePolicyMap() {
$map = array();
foreach ($this->getCustomCapabilities() as $capability => $spec) {
if (empty($spec['template'])) {
continue;
}
if (empty($spec['capability'])) {
continue;
}
$default = $this->getPolicy($capability);
$map[$spec['template']][$spec['capability']] = $default;
}
return $map;
}
public function getApplicationSearchDocumentTypes() {
return array();
}
}
diff --git a/src/applications/celerity/postprocessor/CelerityPostprocessor.php b/src/applications/celerity/postprocessor/CelerityPostprocessor.php
index d80375ebcf..81c1cb8913 100644
--- a/src/applications/celerity/postprocessor/CelerityPostprocessor.php
+++ b/src/applications/celerity/postprocessor/CelerityPostprocessor.php
@@ -1,69 +1,45 @@
<?php
abstract class CelerityPostprocessor
extends Phobject {
private $default;
abstract public function getPostprocessorKey();
abstract public function getPostprocessorName();
abstract public function buildVariables();
public function buildDefaultPostprocessor() {
return new CelerityDefaultPostprocessor();
}
final public function getVariables() {
$variables = $this->buildVariables();
$default = $this->getDefault();
if ($default) {
$variables += $default->getVariables();
}
return $variables;
}
final public function getDefault() {
if ($this->default === null) {
$this->default = $this->buildDefaultPostprocessor();
}
return $this->default;
}
final public static function getPostprocessor($key) {
return idx(self::getAllPostprocessors(), $key);
}
final public static function getAllPostprocessors() {
- static $postprocessors;
-
- if ($postprocessors === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- foreach ($objects as $object) {
- $key = $object->getPostprocessorKey();
- if (empty($map[$key])) {
- $map[$key] = $object;
- continue;
- }
-
- throw new Exception(
- pht(
- 'Two postprocessors (of classes "%s" and "%s") define the same '.
- 'postprocessor key ("%s"). Each postprocessor must define a '.
- 'unique key.',
- get_class($object),
- get_class($map[$key]),
- $key));
- }
- $postprocessors = $map;
- }
-
- return $postprocessors;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getPostprocessorKey')
+ ->execute();
}
}
diff --git a/src/applications/celerity/resources/CelerityPhysicalResources.php b/src/applications/celerity/resources/CelerityPhysicalResources.php
index 730c8dc435..89f0d150a6 100644
--- a/src/applications/celerity/resources/CelerityPhysicalResources.php
+++ b/src/applications/celerity/resources/CelerityPhysicalResources.php
@@ -1,62 +1,49 @@
<?php
/**
* Defines the location of physical static resources which exist at build time
* and are precomputed into a resource map.
*/
abstract class CelerityPhysicalResources extends CelerityResources {
private $map;
abstract public function getPathToMap();
abstract public function findBinaryResources();
abstract public function findTextResources();
public function loadMap() {
if ($this->map === null) {
$this->map = include $this->getPathToMap();
}
return $this->map;
}
public static function getAll() {
static $resources_map;
if ($resources_map === null) {
- $resources_map = array();
-
- $resources_list = id(new PhutilSymbolLoader())
+ $resources_list = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
+ ->setUniqueMethod('getName')
+ ->execute();
foreach ($resources_list as $resources) {
$name = $resources->getName();
if (!preg_match('/^[a-z0-9]+/', $name)) {
throw new Exception(
pht(
'Resources name "%s" is not valid; it must contain only '.
'lowercase latin letters and digits.',
$name));
}
-
- if (empty($resources_map[$name])) {
- $resources_map[$name] = $resources;
- } else {
- $old = get_class($resources_map[$name]);
- $new = get_class($resources);
- throw new Exception(
- pht(
- 'Celerity resource maps must have unique names, but maps %s and '.
- '%s share the same name, "%s".',
- $old,
- $new,
- $name));
- }
}
+
+ $resources_map = $resources_list;
}
return $resources_map;
}
}
diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php
index 2ea2facd4e..4c6c0b581c 100644
--- a/src/applications/config/check/PhabricatorSetupCheck.php
+++ b/src/applications/config/check/PhabricatorSetupCheck.php
@@ -1,176 +1,169 @@
<?php
abstract class PhabricatorSetupCheck extends Phobject {
private $issues;
abstract protected function executeChecks();
const GROUP_OTHER = 'other';
const GROUP_MYSQL = 'mysql';
const GROUP_PHP = 'php';
const GROUP_IMPORTANT = 'important';
public function getExecutionOrder() {
return 1;
}
final protected function newIssue($key) {
$issue = id(new PhabricatorSetupIssue())
->setIssueKey($key);
$this->issues[$key] = $issue;
if ($this->getDefaultGroup()) {
$issue->setGroup($this->getDefaultGroup());
}
return $issue;
}
final public function getIssues() {
return $this->issues;
}
protected function addIssue(PhabricatorSetupIssue $issue) {
$this->issues[$issue->getIssueKey()] = $issue;
return $this;
}
public function getDefaultGroup() {
return null;
}
final public function runSetupChecks() {
$this->issues = array();
$this->executeChecks();
}
final public static function getOpenSetupIssueKeys() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.issue-keys');
}
final public static function setOpenSetupIssueKeys(array $keys) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.issue-keys', $keys);
}
final public static function getUnignoredIssueKeys(array $all_issues) {
assert_instances_of($all_issues, 'PhabricatorSetupIssue');
$keys = array();
foreach ($all_issues as $issue) {
if (!$issue->getIsIgnored()) {
$keys[] = $issue->getIssueKey();
}
}
return $keys;
}
final public static function getConfigNeedsRepair() {
$cache = PhabricatorCaches::getSetupCache();
return $cache->getKey('phabricator.setup.needs-repair');
}
final public static function setConfigNeedsRepair($needs_repair) {
$cache = PhabricatorCaches::getSetupCache();
$cache->setKey('phabricator.setup.needs-repair', $needs_repair);
}
final public static function deleteSetupCheckCache() {
$cache = PhabricatorCaches::getSetupCache();
$cache->deleteKeys(
array(
'phabricator.setup.needs-repair',
'phabricator.setup.issue-keys',
));
}
final public static function willProcessRequest() {
$issue_keys = self::getOpenSetupIssueKeys();
if ($issue_keys === null) {
$issues = self::runAllChecks();
foreach ($issues as $issue) {
if ($issue->getIsFatal()) {
$view = id(new PhabricatorSetupIssueView())
->setIssue($issue);
return id(new PhabricatorConfigResponse())
->setView($view);
}
}
self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues));
}
// Try to repair configuration unless we have a clean bill of health on it.
// We need to keep doing this on every page load until all the problems
// are fixed, which is why it's separate from setup checks (which run
// once per restart).
$needs_repair = self::getConfigNeedsRepair();
if ($needs_repair !== false) {
$needs_repair = self::repairConfig();
self::setConfigNeedsRepair($needs_repair);
}
}
final public static function loadAllChecks() {
- $symbols = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->setConcreteOnly(true)
- ->selectAndLoadSymbols();
-
- $checks = array();
- foreach ($symbols as $symbol) {
- $checks[] = newv($symbol['name'], array());
- }
-
- return msort($checks, 'getExecutionOrder');
+ ->setSortMethod('getExecutionOrder')
+ ->execute();
}
final public static function runAllChecks() {
$checks = self::loadAllChecks();
$issues = array();
foreach ($checks as $check) {
$check->runSetupChecks();
foreach ($check->getIssues() as $key => $issue) {
if (isset($issues[$key])) {
throw new Exception(
pht(
"Two setup checks raised an issue with key '%s'!",
$key));
}
$issues[$key] = $issue;
if ($issue->getIsFatal()) {
break 2;
}
}
}
$ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues');
foreach ($ignore_issues as $ignorable => $derp) {
if (isset($issues[$ignorable])) {
$issues[$ignorable]->setIsIgnored(true);
}
}
return $issues;
}
final public static function repairConfig() {
$needs_repair = false;
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($options as $option) {
try {
$option->getGroup()->validateOption(
$option,
PhabricatorEnv::getEnvConfig($option->getKey()));
} catch (PhabricatorConfigValidationException $ex) {
PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault());
$needs_repair = true;
}
}
return $needs_repair;
}
}
diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
index 770fe10da9..81e7ec80cc 100644
--- a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
+++ b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
@@ -1,81 +1,55 @@
<?php
abstract class PhabricatorDashboardPanelType extends Phobject {
abstract public function getPanelTypeKey();
abstract public function getPanelTypeName();
abstract public function getPanelTypeDescription();
abstract public function getFieldSpecifications();
abstract public function renderPanelContent(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine);
public function initializeFieldsFromRequest(
PhabricatorDashboardPanel $panel,
PhabricatorCustomFieldList $field_list,
AphrontRequest $request) {
return;
}
/**
* Should this panel pull content in over AJAX?
*
* Normally, panels use AJAX to render their content. This makes the page
* interactable sooner, allows panels to render in parallel, and prevents one
* slow panel from slowing everything down.
*
* However, some panels are very cheap to build (i.e., no expensive service
* calls or complicated rendering). In these cases overall performance can be
* improved by disabling async rendering so the panel rendering happens in the
* same process.
*
* @return bool True to enable asynchronous rendering when appropriate.
*/
public function shouldRenderAsync() {
return true;
}
public function adjustPanelHeader(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine,
PHUIHeaderView $header) {
return $header;
}
public static function getAllPanelTypes() {
- static $types;
-
- if ($types === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- foreach ($objects as $object) {
- $key = $object->getPanelTypeKey();
- if (!empty($map[$key])) {
- $this_class = get_class($object);
- $that_class = get_class($map[$key]);
- throw new Exception(
- pht(
- 'Two dashboard panels (of classes "%s" and "%s") have the '.
- 'same panel type key ("%s"). Each panel type must have a '.
- 'unique panel type key.',
- $this_class,
- $that_class,
- $key));
- }
-
- $map[$key] = $object;
- }
-
- $types = $map;
- }
-
- return $types;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getPanelTypeKey')
+ ->execute();
}
}
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
index 0abe12328b..6a6e146cf1 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -1,476 +1,464 @@
<?php
/**
* @task lease Lease Acquisition
* @task resource Resource Allocation
* @task log Logging
*/
abstract class DrydockBlueprintImplementation extends Phobject {
private $activeResource;
private $activeLease;
private $instance;
abstract public function getType();
abstract public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type);
abstract public function isEnabled();
abstract public function getBlueprintName();
abstract public function getDescription();
public function getBlueprintClass() {
return get_class($this);
}
protected function loadLease($lease_id) {
// TODO: Get rid of this?
$query = id(new DrydockLeaseQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($lease_id))
->execute();
$lease = idx($query, $lease_id);
if (!$lease) {
throw new Exception(pht("No such lease '%d'!", $lease_id));
}
return $lease;
}
protected function getInstance() {
if (!$this->instance) {
throw new Exception(
pht('Attach the blueprint instance to the implementation.'));
}
return $this->instance;
}
public function attachInstance(DrydockBlueprint $instance) {
$this->instance = $instance;
return $this;
}
public function getFieldSpecifications() {
return array();
}
public function getDetail($key, $default = null) {
return $this->getInstance()->getDetail($key, $default);
}
/* -( Lease Acquisition )-------------------------------------------------- */
/**
* @task lease
*/
final public function filterResource(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
return $this->canAllocateLease($resource, $lease);
}
/**
* Enforce basic checks on lease/resource compatibility. Allows resources to
* reject leases if they are incompatible, even if the resource types match.
*
* For example, if a resource represents a 32-bit host, this method might
* reject leases that need a 64-bit host. If a resource represents a working
* copy of repository "X", this method might reject leases which need a
* working copy of repository "Y". Generally, although the main types of
* a lease and resource may match (e.g., both "host"), it may not actually be
* possible to satisfy the lease with a specific resource.
*
* This method generally should not enforce limits or perform capacity
* checks. Perform those in @{method:shouldAllocateLease} instead. It also
* should not perform actual acquisition of the lease; perform that in
* @{method:executeAcquireLease} instead.
*
* @param DrydockResource Candidiate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @return bool True if the resource and lease are compatible.
* @task lease
*/
abstract protected function canAllocateLease(
DrydockResource $resource,
DrydockLease $lease);
/**
* @task lease
*/
final public function allocateLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
$this->log(pht('Trying to Allocate Lease'));
$lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING);
$lease->setResourceID($resource->getID());
$lease->attachResource($resource);
$ephemeral_lease = id(clone $lease)->makeEphemeral();
$allocated = false;
$allocation_exception = null;
$resource->openTransaction();
$resource->beginReadLocking();
$resource->reload();
// TODO: Policy stuff.
$other_leases = id(new DrydockLease())->loadAllWhere(
'status IN (%Ld) AND resourceID = %d',
array(
DrydockLeaseStatus::STATUS_ACQUIRING,
DrydockLeaseStatus::STATUS_ACTIVE,
),
$resource->getID());
try {
$allocated = $this->shouldAllocateLease(
$resource,
$ephemeral_lease,
$other_leases);
} catch (Exception $ex) {
$allocation_exception = $ex;
}
if ($allocated) {
$lease->save();
}
$resource->endReadLocking();
if ($allocated) {
$resource->saveTransaction();
$this->log(pht('Allocated Lease'));
} else {
$resource->killTransaction();
$this->log(pht('Failed to Allocate Lease'));
}
if ($allocation_exception) {
$this->logException($allocation_exception);
}
return $allocated;
}
/**
* Enforce lease limits on resources. Allows resources to reject leases if
* they would become over-allocated by accepting them.
*
* For example, if a resource represents disk space, this method might check
* how much space the lease is asking for (say, 200MB) and how much space is
* left unallocated on the resource. It could grant the lease (return true)
* if it has enough remaining space (more than 200MB), and reject the lease
* (return false) if it does not (less than 200MB).
*
* A resource might also allow only exclusive leases. In this case it could
* accept a new lease (return true) if there are no active leases, or reject
* the new lease (return false) if there any other leases.
*
* A lock is held on the resource while this method executes to prevent
* multiple processes from allocating leases on the resource simultaneously.
* However, this means you should implement the method as cheaply as possible.
* In particular, do not perform any actual acquisition or setup in this
* method.
*
* If allocation is permitted, the lease will be moved to `ACQUIRING` status
* and @{method:executeAcquireLease} will be called to actually perform
* acquisition.
*
* General compatibility checks unrelated to resource limits and capacity are
* better implemented in @{method:canAllocateLease}, which serves as a
* cheap filter before lock acquisition.
*
* @param DrydockResource Candidate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @param list<DrydockLease> Other allocated and acquired leases on the
* resource. The implementation can inspect them
* to verify it can safely add the new lease.
* @return bool True to allocate the lease on the resource;
* false to reject it.
* @task lease
*/
abstract protected function shouldAllocateLease(
DrydockResource $resource,
DrydockLease $lease,
array $other_leases);
/**
* @task lease
*/
final public function acquireLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope($resource, $lease);
$this->log(pht('Acquiring Lease'));
$lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE);
$lease->setResourceID($resource->getID());
$lease->attachResource($resource);
$ephemeral_lease = id(clone $lease)->makeEphemeral();
try {
$this->executeAcquireLease($resource, $ephemeral_lease);
} catch (Exception $ex) {
$this->logException($ex);
throw $ex;
}
$lease->setAttributes($ephemeral_lease->getAttributes());
$lease->save();
$this->log(pht('Acquired Lease'));
}
/**
* Acquire and activate an allocated lease. Allows resources to peform setup
* as leases are brought online.
*
* Following a successful call to @{method:canAllocateLease}, a lease is moved
* to `ACQUIRING` status and this method is called after resource locks are
* released. Nothing is locked while this method executes; the implementation
* is free to perform expensive operations like writing files and directories,
* executing commands, etc.
*
* After this method executes, the lease status is moved to `ACTIVE` and the
* original leasee may access it.
*
* If acquisition fails, throw an exception.
*
* @param DrydockResource Resource to acquire a lease on.
* @param DrydockLease Lease to acquire.
* @return void
*/
abstract protected function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease);
final public function releaseLease(
DrydockResource $resource,
DrydockLease $lease) {
$scope = $this->pushActiveScope(null, $lease);
$released = false;
$lease->openTransaction();
$lease->beginReadLocking();
$lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
$lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$lease->save();
$released = true;
}
$lease->endReadLocking();
$lease->saveTransaction();
if (!$released) {
throw new Exception(pht('Unable to release lease: lease not active!'));
}
}
/* -( Resource Allocation )------------------------------------------------ */
public function canAllocateMoreResources(array $pool) {
return true;
}
abstract protected function executeAllocateResource(DrydockLease $lease);
final public function allocateResource(DrydockLease $lease) {
$scope = $this->pushActiveScope(null, $lease);
$this->log(
pht(
"Blueprint '%s': Allocating Resource for '%s'",
$this->getBlueprintClass(),
$lease->getLeaseName()));
try {
$resource = $this->executeAllocateResource($lease);
$this->validateAllocatedResource($resource);
} catch (Exception $ex) {
$this->logException($ex);
throw $ex;
}
return $resource;
}
/* -( Logging )------------------------------------------------------------ */
/**
* @task log
*/
protected function logException(Exception $ex) {
$this->log($ex->getMessage());
}
/**
* @task log
*/
protected function log($message) {
self::writeLog(
$this->activeResource,
$this->activeLease,
$message);
}
/**
* @task log
*/
public static function writeLog(
DrydockResource $resource = null,
DrydockLease $lease = null,
$message = null) {
$log = id(new DrydockLog())
->setEpoch(time())
->setMessage($message);
if ($resource) {
$log->setResourceID($resource->getID());
}
if ($lease) {
$log->setLeaseID($lease->getID());
}
$log->save();
}
public static function getAllBlueprintImplementations() {
- static $list = null;
-
- if ($list === null) {
- $blueprints = id(new PhutilSymbolLoader())
- ->setType('class')
- ->setAncestorClass(__CLASS__)
- ->setConcreteOnly(true)
- ->selectAndLoadSymbols();
- $list = ipull($blueprints, 'name', 'name');
- foreach ($list as $class_name => $ignored) {
- $list[$class_name] = newv($class_name, array());
- }
- }
-
- return $list;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->execute();
}
public static function getAllBlueprintImplementationsForResource($type) {
static $groups = null;
if ($groups === null) {
$groups = mgroup(self::getAllBlueprintImplementations(), 'getType');
}
return idx($groups, $type, array());
}
public static function getNamedImplementation($class) {
return idx(self::getAllBlueprintImplementations(), $class);
}
protected function newResourceTemplate($name) {
$resource = id(new DrydockResource())
->setBlueprintPHID($this->getInstance()->getPHID())
->setBlueprintClass($this->getBlueprintClass())
->setType($this->getType())
->setStatus(DrydockResourceStatus::STATUS_PENDING)
->setName($name)
->save();
$this->activeResource = $resource;
$this->log(
pht(
"Blueprint '%s': Created New Template",
$this->getBlueprintClass()));
return $resource;
}
/**
* Sanity checks that the blueprint is implemented properly.
*/
private function validateAllocatedResource($resource) {
$blueprint = $this->getBlueprintClass();
if (!($resource instanceof DrydockResource)) {
throw new Exception(
pht(
"Blueprint '%s' is not properly implemented: %s must return an ".
"object of type %s or throw, but returned something else.",
$blueprint,
'executeAllocateResource()',
'DrydockResource'));
}
$current_status = $resource->getStatus();
$req_status = DrydockResourceStatus::STATUS_OPEN;
if ($current_status != $req_status) {
$current_name = DrydockResourceStatus::getNameForStatus($current_status);
$req_name = DrydockResourceStatus::getNameForStatus($req_status);
throw new Exception(
pht(
"Blueprint '%s' is not properly implemented: %s must return a %s ".
"with status '%s', but returned one with status '%s'.",
$blueprint,
'executeAllocateResource()',
'DrydockResource',
$req_name,
$current_name));
}
}
private function pushActiveScope(
DrydockResource $resource = null,
DrydockLease $lease = null) {
if (($this->activeResource !== null) ||
($this->activeLease !== null)) {
throw new Exception(pht('There is already an active resource or lease!'));
}
$this->activeResource = $resource;
$this->activeLease = $lease;
return new DrydockBlueprintScopeGuard($this);
}
public function popActiveScope() {
$this->activeResource = null;
$this->activeLease = null;
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php
index 734b3296c0..a87cabf56d 100644
--- a/src/applications/fact/engine/PhabricatorFactEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactEngine.php
@@ -1,39 +1,31 @@
<?php
abstract class PhabricatorFactEngine extends Phobject {
final public static function loadAllEngines() {
- $classes = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->setConcreteOnly(true)
- ->selectAndLoadSymbols();
-
- $objects = array();
- foreach ($classes as $class) {
- $objects[] = newv($class['name'], array());
- }
-
- return $objects;
+ ->execute();
}
public function getFactSpecs(array $fact_types) {
return array();
}
public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
return false;
}
public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
return array();
}
public function shouldComputeAggregateFacts() {
return false;
}
public function computeAggregateFacts() {
return array();
}
}
diff --git a/src/applications/files/engine/PhabricatorFileStorageEngine.php b/src/applications/files/engine/PhabricatorFileStorageEngine.php
index 3ec0ee2fa5..21f7401452 100644
--- a/src/applications/files/engine/PhabricatorFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorFileStorageEngine.php
@@ -1,369 +1,344 @@
<?php
/**
* Defines a storage engine which can write file data somewhere (like a
* database, local disk, Amazon S3, the A:\ drive, or a custom filer) and
* retrieve it later.
*
* You can extend this class to provide new file storage backends.
*
* For more information, see @{article:File Storage Technical Documentation}.
*
* @task construct Constructing an Engine
* @task meta Engine Metadata
* @task file Managing File Data
* @task load Loading Storage Engines
*/
abstract class PhabricatorFileStorageEngine extends Phobject {
/**
* Construct a new storage engine.
*
* @task construct
*/
final public function __construct() {
// <empty>
}
/* -( Engine Metadata )---------------------------------------------------- */
/**
* Return a unique, nonempty string which identifies this storage engine.
* This is used to look up the storage engine when files needs to be read or
* deleted. For instance, if you store files by giving them to a duck for
* safe keeping in his nest down by the pond, you might return 'duck' from
* this method.
*
* @return string Unique string for this engine, max length 32.
* @task meta
*/
abstract public function getEngineIdentifier();
/**
* Prioritize this engine relative to other engines.
*
* Engines with a smaller priority number get an opportunity to write files
* first. Generally, lower-latency filestores should have lower priority
* numbers, and higher-latency filestores should have higher priority
* numbers. Setting priority to approximately the number of milliseconds of
* read latency will generally produce reasonable results.
*
* In conjunction with filesize limits, the goal is to store small files like
* profile images, thumbnails, and text snippets in lower-latency engines,
* and store large files in higher-capacity engines.
*
* @return float Engine priority.
* @task meta
*/
abstract public function getEnginePriority();
/**
* Return `true` if the engine is currently writable.
*
* Engines that are disabled or missing configuration should return `false`
* to prevent new writes. If writes were made with this engine in the past,
* the application may still try to perform reads.
*
* @return bool True if this engine can support new writes.
* @task meta
*/
abstract public function canWriteFiles();
/**
* Return `true` if the engine has a filesize limit on storable files.
*
* The @{method:getFilesizeLimit} method can retrieve the actual limit. This
* method just removes the ambiguity around the meaning of a `0` limit.
*
* @return bool `true` if the engine has a filesize limit.
* @task meta
*/
public function hasFilesizeLimit() {
return true;
}
/**
* Return maximum storable file size, in bytes.
*
* Not all engines have a limit; use @{method:getFilesizeLimit} to check if
* an engine has a limit. Engines without a limit can store files of any
* size.
*
* By default, engines define a limit which supports chunked storage of
* large files. In most cases, you should not change this limit, even if an
* engine has vast storage capacity: chunked storage makes large files more
* manageable and enables features like resumable uploads.
*
* @return int Maximum storable file size, in bytes.
* @task meta
*/
public function getFilesizeLimit() {
// NOTE: This 8MB limit is selected to be larger than the 4MB chunk size,
// but not much larger. Files between 0MB and 8MB will be stored normally;
// files larger than 8MB will be chunked.
return (1024 * 1024 * 8);
}
/**
* Identifies storage engines that support unit tests.
*
* These engines are not used for production writes.
*
* @return bool True if this is a test engine.
* @task meta
*/
public function isTestEngine() {
return false;
}
/**
* Identifies chunking storage engines.
*
* If this is a storage engine which splits files into chunks and stores the
* chunks in other engines, it can return `true` to signal that other
* chunking engines should not try to store data here.
*
* @return bool True if this is a chunk engine.
* @task meta
*/
public function isChunkEngine() {
return false;
}
/* -( Managing File Data )------------------------------------------------- */
/**
* Write file data to the backing storage and return a handle which can later
* be used to read or delete it. For example, if the backing storage is local
* disk, the handle could be the path to the file.
*
* The caller will provide a $params array, which may be empty or may have
* some metadata keys (like "name" and "author") in it. You should be prepared
* to handle writes which specify no metadata, but might want to optionally
* use some keys in this array for debugging or logging purposes. This is
* the same dictionary passed to @{method:PhabricatorFile::newFromFileData},
* so you could conceivably do custom things with it.
*
* If you are unable to write for whatever reason (e.g., the disk is full),
* throw an exception. If there are other satisfactory but less-preferred
* storage engines available, they will be tried.
*
* @param string The file data to write.
* @param array File metadata (name, author), if available.
* @return string Unique string which identifies the stored file, max length
* 255.
* @task file
*/
abstract public function writeFile($data, array $params);
/**
* Read the contents of a file previously written by @{method:writeFile}.
*
* @param string The handle returned from @{method:writeFile} when the
* file was written.
* @return string File contents.
* @task file
*/
abstract public function readFile($handle);
/**
* Delete the data for a file previously written by @{method:writeFile}.
*
* @param string The handle returned from @{method:writeFile} when the
* file was written.
* @return void
* @task file
*/
abstract public function deleteFile($handle);
/* -( Loading Storage Engines )-------------------------------------------- */
/**
* Select viable default storage engines according to configuration. We'll
* select the MySQL and Local Disk storage engines if they are configured
* to allow a given file.
*
* @param int File size in bytes.
* @task load
*/
public static function loadStorageEngines($length) {
$engines = self::loadWritableEngines();
$writable = array();
foreach ($engines as $key => $engine) {
if ($engine->hasFilesizeLimit()) {
$limit = $engine->getFilesizeLimit();
if ($limit < $length) {
continue;
}
}
$writable[$key] = $engine;
}
return $writable;
}
/**
* @task load
*/
public static function loadAllEngines() {
- static $engines;
-
- if ($engines === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- foreach ($objects as $engine) {
- $key = $engine->getEngineIdentifier();
- if (empty($map[$key])) {
- $map[$key] = $engine;
- } else {
- throw new Exception(
- pht(
- 'Storage engines "%s" and "%s" have the same engine '.
- 'identifier "%s". Each storage engine must have a unique '.
- 'identifier.',
- get_class($engine),
- get_class($map[$key]),
- $key));
- }
- }
-
- $map = msort($map, 'getEnginePriority');
-
- $engines = $map;
- }
-
- return $engines;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getEngineIdentifier')
+ ->setSortMethod('getEnginePriority')
+ ->execute();
}
/**
* @task load
*/
private static function loadProductionEngines() {
$engines = self::loadAllEngines();
$active = array();
foreach ($engines as $key => $engine) {
if ($engine->isTestEngine()) {
continue;
}
$active[$key] = $engine;
}
return $active;
}
/**
* @task load
*/
public static function loadWritableEngines() {
$engines = self::loadProductionEngines();
$writable = array();
foreach ($engines as $key => $engine) {
if (!$engine->canWriteFiles()) {
continue;
}
if ($engine->isChunkEngine()) {
// Don't select chunk engines as writable.
continue;
}
$writable[$key] = $engine;
}
return $writable;
}
/**
* @task load
*/
public static function loadWritableChunkEngines() {
$engines = self::loadProductionEngines();
$chunk = array();
foreach ($engines as $key => $engine) {
if (!$engine->canWriteFiles()) {
continue;
}
if (!$engine->isChunkEngine()) {
continue;
}
$chunk[$key] = $engine;
}
return $chunk;
}
/**
* Return the largest file size which can not be uploaded in chunks.
*
* Files smaller than this will always upload in one request, so clients
* can safely skip the allocation step.
*
* @return int|null Byte size, or `null` if there is no chunk support.
*/
public static function getChunkThreshold() {
$engines = self::loadWritableChunkEngines();
$min = null;
foreach ($engines as $engine) {
if (!$min) {
$min = $engine;
continue;
}
if ($min->getChunkSize() > $engine->getChunkSize()) {
$min = $engine->getChunkSize();
}
}
if (!$min) {
return null;
}
return $engine->getChunkSize();
}
public function getFileDataIterator(PhabricatorFile $file, $begin, $end) {
// The default implementation is trivial and just loads the entire file
// upfront.
$data = $file->loadFileData();
if ($begin !== null && $end !== null) {
$data = substr($data, $begin, ($end - $begin));
} else if ($begin !== null) {
$data = substr($data, $begin);
} else if ($end !== null) {
$data = substr($data, 0, $end);
}
return array($data);
}
}
diff --git a/src/applications/files/transform/PhabricatorFileTransform.php b/src/applications/files/transform/PhabricatorFileTransform.php
index caaf46920d..a60a3d4b37 100644
--- a/src/applications/files/transform/PhabricatorFileTransform.php
+++ b/src/applications/files/transform/PhabricatorFileTransform.php
@@ -1,74 +1,52 @@
<?php
abstract class PhabricatorFileTransform extends Phobject {
abstract public function getTransformName();
abstract public function getTransformKey();
abstract public function canApplyTransform(PhabricatorFile $file);
abstract public function applyTransform(PhabricatorFile $file);
public function getDefaultTransform(PhabricatorFile $file) {
return null;
}
public function generateTransforms() {
return array($this);
}
public function executeTransform(PhabricatorFile $file) {
if ($this->canApplyTransform($file)) {
try {
return $this->applyTransform($file);
} catch (Exception $ex) {
// Ignore.
}
}
return $this->getDefaultTransform($file);
}
public static function getAllTransforms() {
- static $map;
-
- if ($map === null) {
- $xforms = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $result = array();
- foreach ($xforms as $xform_template) {
- foreach ($xform_template->generateTransforms() as $xform) {
- $key = $xform->getTransformKey();
- if (isset($result[$key])) {
- throw new Exception(
- pht(
- 'Two %s objects define the same transform key ("%s"), but '.
- 'each transform must have a unique key.',
- __CLASS__,
- $key));
- }
- $result[$key] = $xform;
- }
- }
-
- $map = $result;
- }
-
- return $map;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setExpandMethod('generateTransforms')
+ ->setUniqueMethod('getTransformKey')
+ ->execute();
}
public static function getTransformByKey($key) {
$all = self::getAllTransforms();
$xform = idx($all, $key);
if (!$xform) {
throw new Exception(
pht(
'No file transform with key "%s" exists.',
$key));
}
return $xform;
}
}
diff --git a/src/applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php b/src/applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php
index 67dfb287d4..6545fe92a4 100644
--- a/src/applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php
+++ b/src/applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php
@@ -1,44 +1,19 @@
<?php
abstract class HarbormasterBuildAutoplan extends Phobject {
abstract public function getAutoplanPlanKey();
abstract public function getAutoplanName();
public static function getAutoplan($key) {
return idx(self::getAllAutoplans(), $key);
}
public static function getAllAutoplans() {
- static $plans;
-
- if ($plans === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- foreach ($objects as $object) {
- $key = $object->getAutoplanPlanKey();
- if (!empty($map[$key])) {
- $other = $map[$key];
- throw new Exception(
- pht(
- 'Two build autoplans (of classes "%s" and "%s") define the same '.
- 'key ("%s"). Each autoplan must have a unique key.',
- get_class($other),
- get_class($object),
- $key));
- }
- $map[$key] = $object;
- }
-
- ksort($map);
-
- $plans = $map;
- }
-
- return $plans;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getAutoplanPlanKey')
+ ->execute();
}
}
diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
index 4220faecf1..c831b711dd 100644
--- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php
@@ -1,271 +1,271 @@
<?php
/**
* @task autotarget Automatic Targets
*/
abstract class HarbormasterBuildStepImplementation extends Phobject {
private $settings;
public static function getImplementations() {
- return id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
+ ->execute();
}
public static function getImplementation($class) {
$base = idx(self::getImplementations(), $class);
if ($base) {
return (clone $base);
}
return null;
}
public static function requireImplementation($class) {
if (!$class) {
throw new Exception(pht('No implementation is specified!'));
}
$implementation = self::getImplementation($class);
if (!$implementation) {
throw new Exception(pht('No such implementation "%s" exists!', $class));
}
return $implementation;
}
/**
* The name of the implementation.
*/
abstract public function getName();
/**
* The generic description of the implementation.
*/
public function getGenericDescription() {
return '';
}
/**
* The description of the implementation, based on the current settings.
*/
public function getDescription() {
return $this->getGenericDescription();
}
/**
* Run the build target against the specified build.
*/
abstract public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target);
/**
* Gets the settings for this build step.
*/
public function getSettings() {
return $this->settings;
}
public function getSetting($key, $default = null) {
return idx($this->settings, $key, $default);
}
/**
* Loads the settings for this build step implementation from a build
* step or target.
*/
final public function loadSettings($build_object) {
$this->settings = $build_object->getDetails();
return $this;
}
/**
* Return the name of artifacts produced by this command.
*
* Something like:
*
* return array(
* 'some_name_input_by_user' => 'host');
*
* Future steps will calculate all available artifact mappings
* before them and filter on the type.
*
* @return array The mappings of artifact names to their types.
*/
public function getArtifactInputs() {
return array();
}
public function getArtifactOutputs() {
return array();
}
public function getDependencies(HarbormasterBuildStep $build_step) {
$dependencies = $build_step->getDetail('dependsOn', array());
$inputs = $build_step->getStepImplementation()->getArtifactInputs();
$inputs = ipull($inputs, null, 'key');
$artifacts = $this->getAvailableArtifacts(
$build_step->getBuildPlan(),
$build_step,
null);
foreach ($artifacts as $key => $type) {
if (!array_key_exists($key, $inputs)) {
unset($artifacts[$key]);
}
}
$artifact_steps = ipull($artifacts, 'step');
$artifact_steps = mpull($artifact_steps, 'getPHID');
$dependencies = array_merge($dependencies, $artifact_steps);
return $dependencies;
}
/**
* Returns a list of all artifacts made available in the build plan.
*/
public static function getAvailableArtifacts(
HarbormasterBuildPlan $build_plan,
$current_build_step,
$artifact_type) {
$steps = id(new HarbormasterBuildStepQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuildPlanPHIDs(array($build_plan->getPHID()))
->execute();
$artifacts = array();
$artifact_arrays = array();
foreach ($steps as $step) {
if ($current_build_step !== null &&
$step->getPHID() === $current_build_step->getPHID()) {
continue;
}
$implementation = $step->getStepImplementation();
$array = $implementation->getArtifactOutputs();
$array = ipull($array, 'type', 'key');
foreach ($array as $name => $type) {
if ($type !== $artifact_type && $artifact_type !== null) {
continue;
}
$artifacts[$name] = array('type' => $type, 'step' => $step);
}
}
return $artifacts;
}
/**
* Convert a user-provided string with variables in it, like:
*
* ls ${dirname}
*
* ...into a string with variables merged into it safely:
*
* ls 'dir with spaces'
*
* @param string Name of a `vxsprintf` function, like @{function:vcsprintf}.
* @param string User-provided pattern string containing `${variables}`.
* @param dict List of available replacement variables.
* @return string String with variables replaced safely into it.
*/
protected function mergeVariables($function, $pattern, array $variables) {
$regexp = '/\\$\\{(?P<name>[a-z\\.]+)\\}/';
$matches = null;
preg_match_all($regexp, $pattern, $matches);
$argv = array();
foreach ($matches['name'] as $name) {
if (!array_key_exists($name, $variables)) {
throw new Exception(pht("No such variable '%s'!", $name));
}
$argv[] = $variables[$name];
}
$pattern = str_replace('%', '%%', $pattern);
$pattern = preg_replace($regexp, '%s', $pattern);
return call_user_func($function, $pattern, $argv);
}
public function getFieldSpecifications() {
return array();
}
protected function formatSettingForDescription($key, $default = null) {
return $this->formatValueForDescription($this->getSetting($key, $default));
}
protected function formatValueForDescription($value) {
if (strlen($value)) {
return phutil_tag('strong', array(), $value);
} else {
return phutil_tag('em', array(), pht('(null)'));
}
}
public function supportsWaitForMessage() {
return false;
}
public function shouldWaitForMessage(HarbormasterBuildTarget $target) {
if (!$this->supportsWaitForMessage()) {
return false;
}
return (bool)$target->getDetail('builtin.wait-for-message');
}
protected function shouldAbort(
HarbormasterBuild $build,
HarbormasterBuildTarget $target) {
return $build->getBuildGeneration() !== $target->getBuildGeneration();
}
protected function resolveFuture(
HarbormasterBuild $build,
HarbormasterBuildTarget $target,
Future $future) {
$futures = new FutureIterator(array($future));
foreach ($futures->setUpdateInterval(5) as $key => $future) {
if ($future === null) {
$build->reload();
if ($this->shouldAbort($build, $target)) {
throw new HarbormasterBuildAbortedException();
}
} else {
return $future->resolve();
}
}
}
/* -( Automatic Targets )-------------------------------------------------- */
public function getBuildStepAutotargetStepKey() {
return null;
}
public function getBuildStepAutotargetPlanKey() {
throw new PhutilMethodNotImplementedException();
}
public function shouldRequireAutotargeting() {
return false;
}
}
diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php
index 3f736a9762..da4281c215 100644
--- a/src/applications/herald/adapter/HeraldAdapter.php
+++ b/src/applications/herald/adapter/HeraldAdapter.php
@@ -1,1568 +1,1565 @@
<?php
abstract class HeraldAdapter extends Phobject {
const FIELD_TITLE = 'title';
const FIELD_BODY = 'body';
const FIELD_AUTHOR = 'author';
const FIELD_REVIEWER = 'reviewer';
const FIELD_REVIEWERS = 'reviewers';
const FIELD_COMMITTER = 'committer';
const FIELD_DIFF_FILE = 'diff-file';
const FIELD_DIFF_CONTENT = 'diff-content';
const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content';
const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content';
const FIELD_DIFF_ENORMOUS = 'diff-enormous';
const FIELD_REPOSITORY = 'repository';
const FIELD_REPOSITORY_PROJECTS = 'repository-projects';
const FIELD_AFFECTED_PACKAGE = 'affected-package';
const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
const FIELD_AUTHOR_PROJECTS = 'authorprojects';
const FIELD_PUSHER = 'pusher';
const FIELD_PUSHER_PROJECTS = 'pusher-projects';
const FIELD_DIFFERENTIAL_REVISION = 'differential-revision';
const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const FIELD_DIFFERENTIAL_CCS = 'differential-ccs';
const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted';
const FIELD_IS_MERGE_COMMIT = 'is-merge-commit';
const FIELD_BRANCHES = 'branches';
const FIELD_AUTHOR_RAW = 'author-raw';
const FIELD_COMMITTER_RAW = 'committer-raw';
const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer';
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_UNCONDITIONALLY = 'unconditionally';
const CONDITION_NEVER = 'never';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
const CONDITION_HAS_BIT = 'bit';
const CONDITION_NOT_BIT = '!bit';
const CONDITION_IS_TRUE = 'true';
const CONDITION_IS_FALSE = 'false';
const ACTION_ADD_CC = 'addcc';
const ACTION_REMOVE_CC = 'remcc';
const ACTION_EMAIL = 'email';
const ACTION_NOTHING = 'nothing';
const ACTION_AUDIT = 'audit';
const ACTION_FLAG = 'flag';
const ACTION_ASSIGN_TASK = 'assigntask';
const ACTION_ADD_PROJECTS = 'addprojects';
const ACTION_REMOVE_PROJECTS = 'removeprojects';
const ACTION_ADD_REVIEWERS = 'addreviewers';
const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
const ACTION_BLOCK = 'block';
const ACTION_REQUIRE_SIGNATURE = 'signature';
const VALUE_TEXT = 'text';
const VALUE_NONE = 'none';
const VALUE_EMAIL = 'email';
const VALUE_USER = 'user';
const VALUE_RULE = 'rule';
const VALUE_REPOSITORY = 'repository';
const VALUE_OWNERS_PACKAGE = 'package';
const VALUE_PROJECT = 'project';
const VALUE_FLAG_COLOR = 'flagcolor';
const VALUE_CONTENT_SOURCE = 'contentsource';
const VALUE_USER_OR_PROJECT = 'userorproject';
const VALUE_BUILD_PLAN = 'buildplan';
const VALUE_TASK_PRIORITY = 'taskpriority';
const VALUE_TASK_STATUS = 'taskstatus';
const VALUE_LEGAL_DOCUMENTS = 'legaldocuments';
const VALUE_APPLICATION_EMAIL = 'applicationemail';
const VALUE_SPACE = 'space';
private $contentSource;
private $isNewObject;
private $applicationEmail;
private $customActions = null;
private $queuedTransactions = array();
private $emailPHIDs = array();
private $forcedEmailPHIDs = array();
private $unsubscribedPHIDs;
private $fieldMap;
public function getEmailPHIDs() {
return array_values($this->emailPHIDs);
}
public function getForcedEmailPHIDs() {
return array_values($this->forcedEmailPHIDs);
}
public function getCustomActions() {
if ($this->customActions === null) {
$custom_actions = id(new PhutilSymbolLoader())
->setAncestorClass('HeraldCustomAction')
->loadObjects();
foreach ($custom_actions as $key => $object) {
if (!$object->appliesToAdapter($this)) {
unset($custom_actions[$key]);
}
}
$this->customActions = array();
foreach ($custom_actions as $action) {
$key = $action->getActionKey();
if (array_key_exists($key, $this->customActions)) {
throw new Exception(
pht(
"More than one Herald custom action implementation ".
"handles the action key: '%s'.",
$key));
}
$this->customActions[$key] = $action;
}
}
return $this->customActions;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function getIsNewObject() {
if (is_bool($this->isNewObject)) {
return $this->isNewObject;
}
throw new Exception(
pht(
'You must %s to a boolean first!',
'setIsNewObject()'));
}
public function setIsNewObject($new) {
$this->isNewObject = (bool)$new;
return $this;
}
public function supportsApplicationEmail() {
return false;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function getPHID() {
return $this->getObject()->getPHID();
}
abstract public function getHeraldName();
public function getHeraldField($field_name) {
$impl = $this->getFieldImplementation($field_name);
if ($impl) {
return $impl->getHeraldFieldValue($this->getObject());
}
throw new Exception(pht("Unknown field '%s'!", $field_name));
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$result[] = $this->applyStandardEffect($effect);
}
return $result;
}
protected function handleCustomHeraldEffect(HeraldEffect $effect) {
$custom_action = idx($this->getCustomActions(), $effect->getAction());
if ($custom_action !== null) {
return $custom_action->applyEffect(
$this,
$this->getObject(),
$effect);
}
return null;
}
public function isAvailableToUser(PhabricatorUser $viewer) {
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array($this->getAdapterApplicationClass()))
->execute();
return !empty($applications);
}
public function queueTransaction($transaction) {
$this->queuedTransactions[] = $transaction;
}
public function getQueuedTransactions() {
return $this->queuedTransactions;
}
protected function newTransaction() {
$object = $this->newObject();
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Unable to build a new transaction for adapter object; it does '.
'not implement "%s".',
'PhabricatorApplicationTransactionInterface'));
}
return $object->getApplicationTransactionTemplate();
}
/**
* NOTE: You generally should not override this; it exists to support legacy
* adapters which had hard-coded content types.
*/
public function getAdapterContentType() {
return get_class($this);
}
abstract public function getAdapterContentName();
abstract public function getAdapterContentDescription();
abstract public function getAdapterApplicationClass();
abstract public function getObject();
/**
* Return a new characteristic object for this adapter.
*
* The adapter will use this object to test for interfaces, generate
* transactions, and interact with custom fields.
*
* Adapters must return an object from this method to enable custom
* field rules and various implicit actions.
*
* Normally, you'll return an empty version of the adapted object:
*
* return new ApplicationObject();
*
* @return null|object Template object.
*/
protected function newObject() {
return null;
}
public function supportsRuleType($rule_type) {
return false;
}
public function canTriggerOnObject($object) {
return false;
}
public function explainValidTriggerObjects() {
return pht('This adapter can not trigger on objects.');
}
public function getTriggerObjectPHIDs() {
return array($this->getPHID());
}
public function getAdapterSortKey() {
return sprintf(
'%08d%s',
$this->getAdapterSortOrder(),
$this->getAdapterContentName());
}
public function getAdapterSortOrder() {
return 1000;
}
/* -( Fields )------------------------------------------------------------- */
private function getFieldImplementationMap() {
if ($this->fieldMap === null) {
// We can't use PhutilClassMapQuery here because field expansion
// depends on the adapter and object.
$object = $this->getObject();
$map = array();
$all = HeraldField::getAllFields();
foreach ($all as $key => $field) {
$field = id(clone $field)->setAdapter($this);
if (!$field->supportsObject($object)) {
continue;
}
$subfields = $field->getFieldsForObject($object);
foreach ($subfields as $subkey => $subfield) {
if (isset($map[$subkey])) {
throw new Exception(
pht(
'Two HeraldFields (of classes "%s" and "%s") have the same '.
'field key ("%s") after expansion for an object of class '.
'"%s" inside adapter "%s". Each field must have a unique '.
'field key.',
get_class($subfield),
get_class($map[$subkey]),
$subkey,
get_class($object),
get_class($this)));
}
$subfield = id(clone $subfield)->setAdapter($this);
$map[$subkey] = $subfield;
}
}
$this->fieldMap = $map;
}
return $this->fieldMap;
}
private function getFieldImplementation($key) {
return idx($this->getFieldImplementationMap(), $key);
}
public function getFields() {
return array_keys($this->getFieldImplementationMap());
}
public function getFieldNameMap() {
$map = mpull($this->getFieldImplementationMap(), 'getHeraldFieldName');
return $map + array(
self::FIELD_TITLE => pht('Title'),
self::FIELD_BODY => pht('Body'),
self::FIELD_AUTHOR => pht('Author'),
self::FIELD_COMMITTER => pht('Committer'),
self::FIELD_REVIEWER => pht('Reviewer'),
self::FIELD_REVIEWERS => pht('Reviewers'),
self::FIELD_DIFF_FILE => pht('Any changed filename'),
self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
self::FIELD_REPOSITORY => pht('Repository'),
self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
self::FIELD_AFFECTED_PACKAGE_OWNER =>
pht("Any affected package's owner"),
self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
self::FIELD_PUSHER => pht('Pusher'),
self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
self::FIELD_DIFFERENTIAL_ACCEPTED
=> pht('Accepted Differential revision'),
self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
self::FIELD_BRANCHES => pht('Commit\'s branches'),
self::FIELD_AUTHOR_RAW => pht('Raw author name'),
self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
);
}
/* -( Conditions )--------------------------------------------------------- */
public function getConditionNameMap() {
return array(
self::CONDITION_CONTAINS => pht('contains'),
self::CONDITION_NOT_CONTAINS => pht('does not contain'),
self::CONDITION_IS => pht('is'),
self::CONDITION_IS_NOT => pht('is not'),
self::CONDITION_IS_ANY => pht('is any of'),
self::CONDITION_IS_TRUE => pht('is true'),
self::CONDITION_IS_FALSE => pht('is false'),
self::CONDITION_IS_NOT_ANY => pht('is not any of'),
self::CONDITION_INCLUDE_ALL => pht('include all of'),
self::CONDITION_INCLUDE_ANY => pht('include any of'),
self::CONDITION_INCLUDE_NONE => pht('do not include'),
self::CONDITION_IS_ME => pht('is myself'),
self::CONDITION_IS_NOT_ME => pht('is not myself'),
self::CONDITION_REGEXP => pht('matches regexp'),
self::CONDITION_RULE => pht('matches:'),
self::CONDITION_NOT_RULE => pht('does not match:'),
self::CONDITION_EXISTS => pht('exists'),
self::CONDITION_NOT_EXISTS => pht('does not exist'),
self::CONDITION_UNCONDITIONALLY => '', // don't show anything!
self::CONDITION_NEVER => '', // don't show anything!
self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'),
self::CONDITION_HAS_BIT => pht('has bit'),
self::CONDITION_NOT_BIT => pht('lacks bit'),
);
}
public function getConditionsForField($field) {
$impl = $this->getFieldImplementation($field);
if ($impl) {
return $impl->getHeraldFieldConditions();
}
switch ($field) {
case self::FIELD_TITLE:
case self::FIELD_BODY:
case self::FIELD_COMMITTER_RAW:
case self::FIELD_AUTHOR_RAW:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_NOT_CONTAINS,
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_REGEXP,
);
case self::FIELD_REVIEWER:
case self::FIELD_PUSHER:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
);
case self::FIELD_REPOSITORY:
case self::FIELD_AUTHOR:
case self::FIELD_COMMITTER:
return array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_REVIEWERS:
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_AFFECTED_PACKAGE:
case self::FIELD_AFFECTED_PACKAGE_OWNER:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_DIFF_FILE:
case self::FIELD_BRANCHES:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
);
case self::FIELD_DIFF_CONTENT:
case self::FIELD_DIFF_ADDED_CONTENT:
case self::FIELD_DIFF_REMOVED_CONTENT:
return array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
self::CONDITION_REGEXP_PAIR,
);
case self::FIELD_DIFFERENTIAL_REVIEWERS:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_CCS:
return array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
);
case self::FIELD_DIFFERENTIAL_REVISION:
case self::FIELD_DIFFERENTIAL_ACCEPTED:
return array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
);
case self::FIELD_IS_MERGE_COMMIT:
case self::FIELD_DIFF_ENORMOUS:
case self::FIELD_PUSHER_IS_COMMITTER:
return array(
self::CONDITION_IS_TRUE,
self::CONDITION_IS_FALSE,
);
default:
throw new Exception(
pht(
"This adapter does not define conditions for field '%s'!",
$field));
}
}
public function doesConditionMatch(
HeraldEngine $engine,
HeraldRule $rule,
HeraldCondition $condition,
$field_value) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$field_value as $value) {
if (stripos($value, $condition_value) !== false) {
return true;
}
}
return false;
case self::CONDITION_NOT_CONTAINS:
return (stripos($field_value, $condition_value) === false);
case self::CONDITION_IS:
return ($field_value == $condition_value);
case self::CONDITION_IS_NOT:
return ($field_value != $condition_value);
case self::CONDITION_IS_ME:
return ($field_value == $rule->getAuthorPHID());
case self::CONDITION_IS_NOT_ME:
return ($field_value != $rule->getAuthorPHID());
case self::CONDITION_IS_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return isset($condition_value[$field_value]);
case self::CONDITION_IS_NOT_ANY:
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$condition_value = array_fuse($condition_value);
return !isset($condition_value[$field_value]);
case self::CONDITION_INCLUDE_ALL:
if (!is_array($field_value)) {
throw new HeraldInvalidConditionException(
pht('Object produced non-array value!'));
}
if (!is_array($condition_value)) {
throw new HeraldInvalidConditionException(
pht('Expected condition value to be an array.'));
}
$have = array_select_keys(array_fuse($field_value), $condition_value);
return (count($have) == count($condition_value));
case self::CONDITION_INCLUDE_ANY:
return (bool)array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_INCLUDE_NONE:
return !array_select_keys(
array_fuse($field_value),
$condition_value);
case self::CONDITION_EXISTS:
case self::CONDITION_IS_TRUE:
return (bool)$field_value;
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_IS_FALSE:
return !$field_value;
case self::CONDITION_UNCONDITIONALLY:
return (bool)$field_value;
case self::CONDITION_NEVER:
return false;
case self::CONDITION_REGEXP:
foreach ((array)$field_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($condition_value.'S', $value);
if ($result === false) {
throw new HeraldInvalidConditionException(
pht('Regular expression is not valid!'));
}
if ($result) {
return true;
}
}
return false;
case self::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = null;
try {
$regexp_pair = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not valid JSON!'));
}
if (count($regexp_pair) != 2) {
throw new HeraldInvalidConditionException(
pht('Regular expression pair is not a pair!'));
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$field_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
throw new HeraldInvalidConditionException(
pht('First regular expression is invalid!'));
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
throw new HeraldInvalidConditionException(
pht('Second regular expression is invalid!'));
}
if ($value_matches) {
return true;
}
}
}
return false;
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
$rule = $engine->getRule($condition_value);
if (!$rule) {
throw new HeraldInvalidConditionException(
pht('Condition references a rule which does not exist!'));
}
$is_not = ($condition_type == self::CONDITION_NOT_RULE);
$result = $engine->doesRuleMatch($rule, $this);
if ($is_not) {
$result = !$result;
}
return $result;
case self::CONDITION_HAS_BIT:
return (($condition_value & $field_value) === (int)$condition_value);
case self::CONDITION_NOT_BIT:
return (($condition_value & $field_value) !== (int)$condition_value);
default:
throw new HeraldInvalidConditionException(
pht("Unknown condition '%s'.", $condition_type));
}
}
public function willSaveCondition(HeraldCondition $condition) {
$condition_type = $condition->getFieldCondition();
$condition_value = $condition->getValue();
switch ($condition_type) {
case self::CONDITION_REGEXP:
$ok = @preg_match($condition_value, '');
if ($ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression "%s" is not valid. Regular expressions '.
'must have enclosing characters (e.g. "@/path/to/file@", not '.
'"/path/to/file") and be syntactically correct.',
$condition_value));
}
break;
case self::CONDITION_REGEXP_PAIR:
$json = null;
try {
$json = phutil_json_decode($condition_value);
} catch (PhutilJSONParserException $ex) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" is not valid JSON. Enter a '.
'valid JSON array with two elements.',
$condition_value));
}
if (count($json) != 2) {
throw new HeraldInvalidConditionException(
pht(
'The regular expression pair "%s" must have exactly two '.
'elements.',
$condition_value));
}
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
$key_ok = @preg_match($key_regexp, '');
if ($key_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The first regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$key_regexp));
}
$val_ok = @preg_match($val_regexp, '');
if ($val_ok === false) {
throw new HeraldInvalidConditionException(
pht(
'The second regexp in the regexp pair, "%s", is not a valid '.
'regexp.',
$val_regexp));
}
break;
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_RULE:
case self::CONDITION_NOT_RULE:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_HAS_BIT:
case self::CONDITION_NOT_BIT:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
// No explicit validation for these types, although there probably
// should be in some cases.
break;
default:
throw new HeraldInvalidConditionException(
pht(
'Unknown condition "%s"!',
$condition_type));
}
}
/* -( Actions )------------------------------------------------------------ */
public function getCustomActionsForRuleType($rule_type) {
$results = array();
foreach ($this->getCustomActions() as $custom_action) {
if ($custom_action->appliesToRuleType($rule_type)) {
$results[] = $custom_action;
}
}
return $results;
}
public function getActions($rule_type) {
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$custom_actions = mpull($custom_actions, 'getActionKey');
$actions = $custom_actions;
$object = $this->newObject();
if (($object instanceof PhabricatorProjectInterface)) {
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
$actions[] = self::ACTION_ADD_PROJECTS;
$actions[] = self::ACTION_REMOVE_PROJECTS;
}
}
return $actions;
}
public function getActionNameMap($rule_type) {
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add Subscribers'),
self::ACTION_REMOVE_CC => pht('Remove Subscribers'),
self::ACTION_EMAIL => pht('Send an email to'),
self::ACTION_AUDIT => pht('Trigger an Audit by'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to'),
self::ACTION_ADD_PROJECTS => pht('Add projects'),
self::ACTION_REMOVE_PROJECTS => pht('Remove projects'),
self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'),
self::ACTION_BLOCK => pht('Block change with message'),
);
break;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$standard = array(
self::ACTION_NOTHING => pht('Do nothing'),
self::ACTION_ADD_CC => pht('Add me as a subscriber'),
self::ACTION_REMOVE_CC => pht('Remove me as a subscriber'),
self::ACTION_EMAIL => pht('Send me an email'),
self::ACTION_AUDIT => pht('Trigger an Audit by me'),
self::ACTION_FLAG => pht('Mark with flag'),
self::ACTION_ASSIGN_TASK => pht('Assign task to me'),
self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
self::ACTION_ADD_BLOCKING_REVIEWERS =>
pht('Add me as a blocking reviewer'),
);
break;
default:
throw new Exception(pht("Unknown rule type '%s'!", $rule_type));
}
$custom_actions = $this->getCustomActionsForRuleType($rule_type);
$standard += mpull($custom_actions, 'getActionName', 'getActionKey');
return $standard;
}
public function willSaveAction(
HeraldRule $rule,
HeraldAction $action) {
$target = $action->getTarget();
if (is_array($target)) {
$target = array_keys($target);
}
$author_phid = $rule->getAuthorPHID();
$rule_type = $rule->getRuleType();
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
switch ($action->getAction()) {
case self::ACTION_EMAIL:
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
// For personal rules, force these actions to target the rule owner.
$target = array($author_phid);
break;
case self::ACTION_FLAG:
// Make sure flag color is valid; set to blue if not.
$color_map = PhabricatorFlagColor::getColorNameMap();
if (empty($color_map[$target])) {
$target = PhabricatorFlagColor::COLOR_BLUE;
}
break;
case self::ACTION_BLOCK:
case self::ACTION_NOTHING:
break;
default:
throw new HeraldInvalidActionException(
pht(
'Unrecognized action type "%s"!',
$action->getAction()));
}
}
$action->setTarget($target);
}
/* -( Values )------------------------------------------------------------- */
public function getValueTypeForFieldAndCondition($field, $condition) {
$impl = $this->getFieldImplementation($field);
if ($impl) {
return $impl->getHeraldFieldValueType($condition);
}
switch ($condition) {
case self::CONDITION_CONTAINS:
case self::CONDITION_NOT_CONTAINS:
case self::CONDITION_REGEXP:
case self::CONDITION_REGEXP_PAIR:
return self::VALUE_TEXT;
case self::CONDITION_IS:
case self::CONDITION_IS_NOT:
return self::VALUE_TEXT;
case self::CONDITION_IS_ANY:
case self::CONDITION_IS_NOT_ANY:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_INCLUDE_ALL:
case self::CONDITION_INCLUDE_ANY:
case self::CONDITION_INCLUDE_NONE:
switch ($field) {
case self::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case self::FIELD_AFFECTED_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
case self::FIELD_AUTHOR_PROJECTS:
case self::FIELD_PUSHER_PROJECTS:
case self::FIELD_REPOSITORY_PROJECTS:
return self::VALUE_PROJECT;
case self::FIELD_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
default:
return self::VALUE_USER;
}
break;
case self::CONDITION_IS_ME:
case self::CONDITION_IS_NOT_ME:
case self::CONDITION_EXISTS:
case self::CONDITION_NOT_EXISTS:
case self::CONDITION_UNCONDITIONALLY:
case self::CONDITION_NEVER:
case self::CONDITION_IS_TRUE:
case self::CONDITION_IS_FALSE:
return self::VALUE_NONE;
default:
throw new Exception(pht("Unknown condition '%s'.", $condition));
}
}
public function getValueTypeForAction($action, $rule_type) {
$is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
if ($is_personal) {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
case self::ACTION_NOTHING:
case self::ACTION_AUDIT:
case self::ACTION_ASSIGN_TASK:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_NONE;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
}
} else {
switch ($action) {
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
case self::ACTION_EMAIL:
return self::VALUE_EMAIL;
case self::ACTION_NOTHING:
return self::VALUE_NONE;
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return self::VALUE_PROJECT;
case self::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
case self::ACTION_ASSIGN_TASK:
return self::VALUE_USER;
case self::ACTION_AUDIT:
case self::ACTION_ADD_REVIEWERS:
case self::ACTION_ADD_BLOCKING_REVIEWERS:
return self::VALUE_USER_OR_PROJECT;
case self::ACTION_APPLY_BUILD_PLANS:
return self::VALUE_BUILD_PLAN;
case self::ACTION_REQUIRE_SIGNATURE:
return self::VALUE_LEGAL_DOCUMENTS;
case self::ACTION_BLOCK:
return self::VALUE_TEXT;
}
}
$custom_action = idx($this->getCustomActions(), $action);
if ($custom_action !== null) {
return $custom_action->getActionType();
}
throw new Exception(pht("Unknown or invalid action '%s'.", $action));
}
/* -( Repetition )--------------------------------------------------------- */
public function getRepetitionOptions() {
return array(
HeraldRepetitionPolicyConfig::EVERY,
);
}
abstract protected function initializeNewAdapter();
/**
* Does this adapter's event fire only once?
*
* Single use adapters (like pre-commit and diff adapters) only fire once,
* so fields like "Is new object" don't make sense to apply to their content.
*
* @return bool
*/
public function isSingleEventAdapter() {
return false;
}
public static function getAllAdapters() {
- static $adapters;
- if (!$adapters) {
- $adapters = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
- $adapters = msort($adapters, 'getAdapterSortKey');
- }
- return $adapters;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getAdapterContentType')
+ ->setSortMethod('getAdapterSortKey')
+ ->execute();
}
public static function getAdapterForContentType($content_type) {
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if ($adapter->getAdapterContentType() == $content_type) {
$adapter = id(clone $adapter);
$adapter->initializeNewAdapter();
return $adapter;
}
}
throw new Exception(
pht(
'No adapter exists for Herald content type "%s".',
$content_type));
}
public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
$map = array();
$adapters = self::getAllAdapters();
foreach ($adapters as $adapter) {
if (!$adapter->isAvailableToUser($viewer)) {
continue;
}
$type = $adapter->getAdapterContentType();
$name = $adapter->getAdapterContentName();
$map[$type] = $name;
}
return $map;
}
public function getEditorValueForCondition(
PhabricatorUser $viewer,
HeraldCondition $condition,
array $handles) {
$impl = $this->getFieldImplementation($condition->getFieldName());
if ($impl) {
return $impl->getEditorValue(
$viewer,
$condition->getValue());
}
$value = $condition->getValue();
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $phid) {
$value_map[$phid] = $handles[$phid]->getName();
}
$value = $value_map;
}
return $value;
}
public function renderRuleAsText(
HeraldRule $rule,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
require_celerity_resource('herald-css');
$icon = id(new PHUIIconView())
->setIconFont('fa-chevron-circle-right lightgreytext')
->addClass('herald-list-icon');
if ($rule->getMustMatchAll()) {
$match_text = pht('When all of these conditions are met:');
} else {
$match_text = pht('When any of these conditions are met:');
}
$match_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$match_text);
$match_list = array();
foreach ($rule->getConditions() as $condition) {
$match_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderConditionAsText($condition, $handles, $viewer),
));
}
$integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::EVERY);
if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
$action_text =
pht('Take these actions every time this rule matches:');
} else {
$action_text =
pht('Take these actions the first time this rule matches:');
}
$action_title = phutil_tag(
'p',
array(
'class' => 'herald-list-description',
),
$action_text);
$action_list = array();
foreach ($rule->getActions() as $action) {
$action_list[] = phutil_tag(
'div',
array(
'class' => 'herald-list-item',
),
array(
$icon,
$this->renderActionAsText($action, $handles),
));
}
return array(
$match_title,
$match_list,
$action_title,
$action_list,
);
}
private function renderConditionAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$field_type = $condition->getFieldName();
$default = pht('(Unknown Field "%s")', $field_type);
$field_name = idx($this->getFieldNameMap(), $field_type, $default);
$condition_type = $condition->getFieldCondition();
$condition_name = idx($this->getConditionNameMap(), $condition_type);
$value = $this->renderConditionValueAsText($condition, $handles, $viewer);
return hsprintf(' %s %s %s', $field_name, $condition_name, $value);
}
private function renderActionAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
$action_type = $action->getAction();
$default = pht('(Unknown Action "%s") equals', $action_type);
$action_name = idx(
$this->getActionNameMap($rule_global),
$action_type,
$default);
$target = $this->renderActionTargetAsText($action, $handles);
return hsprintf(' %s %s', $action_name, $target);
}
private function renderConditionValueAsText(
HeraldCondition $condition,
PhabricatorHandleList $handles,
PhabricatorUser $viewer) {
$impl = $this->getFieldImplementation($condition->getFieldName());
if ($impl) {
return $impl->renderConditionValue(
$viewer,
$condition->getValue());
}
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
switch ($condition->getFieldName()) {
case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
$change_map =
PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
foreach ($value as $index => $val) {
$name = idx($change_map, $val);
if ($name) {
$value[$index] = $name;
}
}
break;
default:
foreach ($value as $index => $val) {
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$value[$index] = $handle->renderLink();
}
}
break;
}
$value = phutil_implode_html(', ', $value);
return $value;
}
private function renderActionTargetAsText(
HeraldAction $action,
PhabricatorHandleList $handles) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $index => $val) {
switch ($action->getAction()) {
case self::ACTION_FLAG:
$target[$index] = PhabricatorFlagColor::getColorName($val);
break;
default:
$handle = $handles->getHandleIfExists($val);
if ($handle) {
$target[$index] = $handle->renderLink();
}
break;
}
}
$target = phutil_implode_html(', ', $target);
return $target;
}
/**
* Given a @{class:HeraldRule}, this function extracts all the phids that
* we'll want to load as handles later.
*
* This function performs a somewhat hacky approach to figuring out what
* is and is not a phid - try to get the phid type and if the type is
* *not* unknown assume its a valid phid.
*
* Don't try this at home. Use more strongly typed data at home.
*
* Think of the children.
*/
public static function getHandlePHIDs(HeraldRule $rule) {
$phids = array($rule->getAuthorPHID());
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
foreach ($rule->getActions() as $action) {
$target = $action->getTarget();
if (!is_array($target)) {
$target = array($target);
}
foreach ($target as $val) {
if (phid_get_type($val) !=
PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
$phids[] = $val;
}
}
}
if ($rule->isObjectRule()) {
$phids[] = $rule->getTriggerObjectPHID();
}
return $phids;
}
/* -( Applying Effects )--------------------------------------------------- */
/**
* @task apply
*/
protected function applyStandardEffect(HeraldEffect $effect) {
$action = $effect->getAction();
$rule_type = $effect->getRule()->getRuleType();
$supported = $this->getActions($rule_type);
$supported = array_fuse($supported);
if (empty($supported[$action])) {
return new HeraldApplyTranscript(
$effect,
false,
pht(
'Adapter "%s" does not support action "%s" for rule type "%s".',
get_class($this),
$action,
$rule_type));
}
switch ($action) {
case self::ACTION_ADD_PROJECTS:
case self::ACTION_REMOVE_PROJECTS:
return $this->applyProjectsEffect($effect);
case self::ACTION_ADD_CC:
case self::ACTION_REMOVE_CC:
return $this->applySubscribersEffect($effect);
case self::ACTION_FLAG:
return $this->applyFlagEffect($effect);
case self::ACTION_EMAIL:
return $this->applyEmailEffect($effect);
case self::ACTION_NOTHING:
return $this->applyNothingEffect($effect);
default:
break;
}
$result = $this->handleCustomHeraldEffect($effect);
if (!$result) {
return new HeraldApplyTranscript(
$effect,
false,
pht(
'No custom action exists to handle rule action "%s".',
$action));
}
return $result;
}
private function applyNothingEffect(HeraldEffect $effect) {
return new HeraldApplyTranscript(
$effect,
true,
pht('Did nothing.'));
}
/**
* @task apply
*/
private function applyProjectsEffect(HeraldEffect $effect) {
if ($effect->getAction() == self::ACTION_ADD_PROJECTS) {
$kind = '+';
} else {
$kind = '-';
}
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$project_phids = $effect->getTarget();
$xaction = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $project_type)
->setNewValue(
array(
$kind => array_fuse($project_phids),
));
$this->queueTransaction($xaction);
return new HeraldApplyTranscript(
$effect,
true,
pht('Added projects.'));
}
/**
* @task apply
*/
private function applySubscribersEffect(HeraldEffect $effect) {
if ($effect->getAction() == self::ACTION_ADD_CC) {
$kind = '+';
$is_add = true;
} else {
$kind = '-';
$is_add = false;
}
$subscriber_phids = array_fuse($effect->getTarget());
if (!$subscriber_phids) {
return new HeraldApplyTranscript(
$effect,
false,
pht('This action lists no users or objects to affect.'));
}
// The "Add Subscribers" rule only adds subscribers who haven't previously
// unsubscribed from the object explicitly. Filter these subscribers out
// before continuing.
$unsubscribed = array();
if ($is_add) {
if ($this->unsubscribedPHIDs === null) {
$this->unsubscribedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getObject()->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
}
foreach ($this->unsubscribedPHIDs as $phid) {
if (isset($subscriber_phids[$phid])) {
$unsubscribed[$phid] = $phid;
unset($subscriber_phids[$phid]);
}
}
}
if (!$subscriber_phids) {
return new HeraldApplyTranscript(
$effect,
false,
pht('All targets have previously unsubscribed explicitly.'));
}
// Filter out PHIDs which aren't valid subscribers. Lower levels of the
// stack will fail loudly if we try to add subscribers with invalid PHIDs
// or unknown PHID types, so drop them here.
$invalid = array();
foreach ($subscriber_phids as $phid) {
$type = phid_get_type($phid);
switch ($type) {
case PhabricatorPeopleUserPHIDType::TYPECONST:
case PhabricatorProjectProjectPHIDType::TYPECONST:
break;
default:
$invalid[$phid] = $phid;
unset($subscriber_phids[$phid]);
break;
}
}
if (!$subscriber_phids) {
return new HeraldApplyTranscript(
$effect,
false,
pht('All targets are invalid as subscribers.'));
}
$xaction = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
$kind => $subscriber_phids,
));
$this->queueTransaction($xaction);
// TODO: We could be more detailed about this, but doing it meaningfully
// probably requires substantial changes to how transactions are rendered
// first.
if ($is_add) {
$message = pht('Subscribed targets.');
} else {
$message = pht('Unsubscribed targets.');
}
return new HeraldApplyTranscript($effect, true, $message);
}
/**
* @task apply
*/
private function applyFlagEffect(HeraldEffect $effect) {
$phid = $this->getPHID();
$color = $effect->getTarget();
$rule = $effect->getRule();
$user = $rule->getAuthor();
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if ($flag) {
return new HeraldApplyTranscript(
$effect,
false,
pht('Object already flagged.'));
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
// TOOD: Should really be transcript PHID, but it doesn't exist yet.
$flag->setReasonPHID($user->getPHID());
$flag->setColor($color);
$flag->setNote(
pht('Flagged by Herald Rule "%s".', $rule->getName()));
$flag->save();
return new HeraldApplyTranscript(
$effect,
true,
pht('Added flag.'));
}
/**
* @task apply
*/
private function applyEmailEffect(HeraldEffect $effect) {
foreach ($effect->getTarget() as $phid) {
$this->emailPHIDs[$phid] = $phid;
// If this is a personal rule, we'll force delivery of a real email. This
// effect is stronger than notification preferences, so you get an actual
// email even if your preferences are set to "Notify" or "Ignore".
$rule = $effect->getRule();
if ($rule->isPersonalRule()) {
$this->forcedEmailPHIDs[$phid] = $phid;
}
}
return new HeraldApplyTranscript(
$effect,
true,
pht('Added mailable to mail targets.'));
}
}
diff --git a/src/applications/maniphest/export/ManiphestExcelFormat.php b/src/applications/maniphest/export/ManiphestExcelFormat.php
index 29c94f475a..e455a63656 100644
--- a/src/applications/maniphest/export/ManiphestExcelFormat.php
+++ b/src/applications/maniphest/export/ManiphestExcelFormat.php
@@ -1,44 +1,35 @@
<?php
abstract class ManiphestExcelFormat extends Phobject {
final public static function loadAllFormats() {
- $classes = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->setConcreteOnly(true)
- ->selectAndLoadSymbols();
-
- $objects = array();
- foreach ($classes as $class) {
- $objects[$class['name']] = newv($class['name'], array());
- }
-
- $objects = msort($objects, 'getOrder');
-
- return $objects;
+ ->setSortMethod('getOrder')
+ ->execute();
}
abstract public function getName();
abstract public function getFileName();
public function getOrder() {
return 0;
}
protected function computeExcelDate($epoch) {
$seconds_per_day = (60 * 60 * 24);
$offset = ($seconds_per_day * 25569);
return ($epoch + $offset) / $seconds_per_day;
}
/**
* @phutil-external-symbol class PHPExcel
*/
abstract public function buildWorkbook(
PHPExcel $workbook,
array $tasks,
array $handles,
PhabricatorUser $user);
}
diff --git a/src/applications/meta/panel/PhabricatorApplicationConfigurationPanel.php b/src/applications/meta/panel/PhabricatorApplicationConfigurationPanel.php
index 1b72b3f5c2..358a6f753b 100644
--- a/src/applications/meta/panel/PhabricatorApplicationConfigurationPanel.php
+++ b/src/applications/meta/panel/PhabricatorApplicationConfigurationPanel.php
@@ -1,88 +1,71 @@
<?php
abstract class PhabricatorApplicationConfigurationPanel
extends Phobject {
private $viewer;
private $application;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setApplication(PhabricatorApplication $application) {
$this->application = $application;
return $this;
}
public function getApplication() {
return $this->application;
}
public function getPanelURI($path = null) {
$app_key = get_class($this->getApplication());
$panel_key = $this->getPanelKey();
$base = "/applications/panel/{$app_key}/{$panel_key}/";
return $base.ltrim($path, '/');
}
/**
* Return a short, unique string key which identifies this panel.
*
* This key is used in URIs. Good values might be "email" or "files".
*/
abstract public function getPanelKey();
abstract public function shouldShowForApplication(
PhabricatorApplication $application);
abstract public function buildConfigurationPagePanel();
abstract public function handlePanelRequest(
AphrontRequest $request,
PhabricatorController $controller);
public static function loadAllPanels() {
- $objects = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $panels = array();
- foreach ($objects as $object) {
- $key = $object->getPanelKey();
- if (empty($panels[$key])) {
- $panels[$key] = $object;
- } else {
- throw new Exception(
- pht(
- 'Application configuration panels "%s" and "%s" have the same '.
- 'panel key, "%s". Each panel must have a unique key.',
- get_class($object),
- get_class($panels[$key]),
- $key));
- }
- }
-
- return $panels;
+ ->setUniqueMethod('getPanelKey')
+ ->execute();
}
public static function loadAllPanelsForApplication(
PhabricatorApplication $application) {
$panels = self::loadAllPanels();
$application_panels = array();
foreach ($panels as $key => $panel) {
if (!$panel->shouldShowForApplication($application)) {
continue;
}
$application_panels[$key] = $panel;
}
return $application_panels;
}
}
diff --git a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
index cc78df3492..4a7102595f 100644
--- a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
+++ b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php
@@ -1,120 +1,110 @@
<?php
/**
* @task docs Command Documentation
*/
abstract class MetaMTAEmailTransactionCommand extends Phobject {
abstract public function getCommand();
/**
* Return a brief human-readable description of the command effect.
*
* This should normally be one or two sentences briefly describing the
* command behavior.
*
* @return string Brief human-readable remarkup.
* @task docs
*/
abstract public function getCommandSummary();
/**
* Return a one-line Remarkup description of command syntax for documentation.
*
* @return string Brief human-readable remarkup.
* @task docs
*/
public function getCommandSyntax() {
return '**!'.$this->getCommand().'**';
}
/**
* Return a longer human-readable description of the command effect.
*
* This can be as long as necessary to explain the command.
*
* @return string Human-readable remarkup of whatever length is desired.
* @task docs
*/
public function getCommandDescription() {
return null;
}
abstract public function isCommandSupportedForObject(
PhabricatorApplicationTransactionInterface $object);
abstract public function buildTransactions(
PhabricatorUser $viewer,
PhabricatorApplicationTransactionInterface $object,
PhabricatorMetaMTAReceivedMail $mail,
$command,
array $argv);
public function getCommandAliases() {
return array();
}
public function getCommandObjects() {
return array($this);
}
public static function getAllCommands() {
- static $commands;
-
- if ($commands === null) {
- $kinds = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
- $commands = array();
- foreach ($kinds as $kind) {
- foreach ($kind->getCommandObjects() as $command) {
- $commands[] = $command;
- }
- }
- }
-
- return $commands;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setExpandMethod('getCommandObjects')
+ ->setUniqueMethod('getCommand')
+ ->execute();
}
public static function getAllCommandsForObject(
PhabricatorApplicationTransactionInterface $object) {
$commands = self::getAllCommands();
foreach ($commands as $key => $command) {
if (!$command->isCommandSupportedForObject($object)) {
unset($commands[$key]);
}
}
return $commands;
}
public static function getCommandMap(array $commands) {
assert_instances_of($commands, __CLASS__);
$map = array();
foreach ($commands as $command) {
$keywords = $command->getCommandAliases();
$keywords[] = $command->getCommand();
foreach ($keywords as $keyword) {
$keyword = phutil_utf8_strtolower($keyword);
if (empty($map[$keyword])) {
$map[$keyword] = $command;
} else {
throw new Exception(
pht(
'Mail commands "%s" and "%s" both respond to keyword "%s". '.
'Keywords must be uniquely associated with commands.',
get_class($command),
get_class($map[$keyword]),
$keyword));
}
}
}
return $map;
}
}
diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/NuanceSourceDefinition.php
index b96b161e3a..621fa615f9 100644
--- a/src/applications/nuance/source/NuanceSourceDefinition.php
+++ b/src/applications/nuance/source/NuanceSourceDefinition.php
@@ -1,310 +1,289 @@
<?php
/**
* @task action Handling Action Requests
*/
abstract class NuanceSourceDefinition extends Phobject {
private $actor;
private $sourceObject;
public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
public function getActor() {
return $this->actor;
}
public function requireActor() {
$actor = $this->getActor();
if (!$actor) {
throw new PhutilInvalidStateException('setActor');
}
return $actor;
}
public function setSourceObject(NuanceSource $source) {
$source->setType($this->getSourceTypeConstant());
$this->sourceObject = $source;
return $this;
}
public function getSourceObject() {
return $this->sourceObject;
}
public function requireSourceObject() {
$source = $this->getSourceObject();
if (!$source) {
throw new PhutilInvalidStateException('setSourceObject');
}
return $source;
}
/**
* Gives a @{class:NuanceSourceDefinition} object for a given
* @{class:NuanceSource}. Note you still need to @{method:setActor}
* before the @{class:NuanceSourceDefinition} object will be useful.
*/
public static function getDefinitionForSource(NuanceSource $source) {
$definitions = self::getAllDefinitions();
$map = mpull($definitions, null, 'getSourceTypeConstant');
$definition = $map[$source->getType()];
$definition->setSourceObject($source);
return $definition;
}
public static function getAllDefinitions() {
- static $definitions;
-
- if ($definitions === null) {
- $definitions = array();
-
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
- foreach ($objects as $definition) {
- $key = $definition->getSourceTypeConstant();
- $name = $definition->getName();
- if (isset($definitions[$key])) {
- $conflict = $definitions[$key];
- throw new Exception(
- pht(
- 'Definition %s conflicts with definition %s. This is a '.
- 'programming error.',
- $conflict,
- $name));
- }
- $definitions[$key] = $definition;
- }
- }
-
- return $definitions;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getSourceTypeConstant')
+ ->execute();
}
/**
* A human readable string like "Twitter" or "Phabricator Form".
*/
abstract public function getName();
/**
* Human readable description of this source, a sentence or two long.
*/
abstract public function getSourceDescription();
/**
* This should be a any VARCHAR(32).
*
* @{method:getAllDefinitions} will throw if you choose a string that
* collides with another @{class:NuanceSourceDefinition} class.
*/
abstract public function getSourceTypeConstant();
/**
* Code to create and update @{class:NuanceItem}s and
* @{class:NuanceRequestor}s via daemons goes here.
*
* If that does not make sense for the @{class:NuanceSource} you are
* defining, simply return null. For example,
* @{class:NuancePhabricatorFormSourceDefinition} since these are one-way
* contact forms.
*/
abstract public function updateItems();
private function loadSourceObjectPolicies(
PhabricatorUser $user,
NuanceSource $source) {
$user = $this->requireActor();
$source = $this->requireSourceObject();
return id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($source)
->execute();
}
final public function getEditTitle() {
$source = $this->requireSourceObject();
if ($source->getPHID()) {
$title = pht('Edit "%s" source.', $source->getName());
} else {
$title = pht('Create a new "%s" source.', $this->getName());
}
return $title;
}
final public function buildEditLayout(AphrontRequest $request) {
$actor = $this->requireActor();
$source = $this->requireSourceObject();
$form_errors = array();
$error_messages = array();
$transactions = array();
$validation_exception = null;
if ($request->isFormPost()) {
$transactions = $this->buildTransactions($request);
try {
$editor = id(new NuanceSourceEditor())
->setActor($actor)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($source, $transactions);
return id(new AphrontRedirectResponse())
->setURI($source->getURI());
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$form = $this->renderEditForm($validation_exception);
$layout = id(new PHUIObjectBoxView())
->setHeaderText($this->getEditTitle())
->setValidationException($validation_exception)
->setFormErrors($error_messages)
->setForm($form);
return $layout;
}
/**
* Code to create a form to edit the @{class:NuanceItem} you are defining.
*
* return @{class:AphrontFormView}
*/
private function renderEditForm(
PhabricatorApplicationTransactionValidationException $ex = null) {
$user = $this->requireActor();
$source = $this->requireSourceObject();
$policies = $this->loadSourceObjectPolicies($user, $source);
$e_name = null;
if ($ex) {
$e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME);
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setError($e_name)
->setValue($source->getName()));
$form = $this->augmentEditForm($form, $ex);
$form
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($source)
->setPolicies($policies)
->setName('viewPolicy'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($source)
->setPolicies($policies)
->setName('editPolicy'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($source->getURI())
->setValue(pht('Save')));
return $form;
}
/**
* return @{class:AphrontFormView}
*/
protected function augmentEditForm(
AphrontFormView $form,
PhabricatorApplicationTransactionValidationException $ex = null) {
return $form;
}
/**
* Hook to build up @{class:PhabricatorTransactions}.
*
* return array $transactions
*/
protected function buildTransactions(AphrontRequest $request) {
$transactions = array();
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($request->getStr('editPolicy'));
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getStr('viewPolicy'));
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(NuanceSourceTransaction::TYPE_NAME)
->setNewvalue($request->getStr('name'));
return $transactions;
}
abstract public function renderView();
abstract public function renderListView();
protected function newItemFromProperties(
NuanceRequestor $requestor,
array $properties,
PhabricatorContentSource $content_source) {
// TODO: Should we have a tighter actor/viewer model? Requestors will
// often have no real user associated with them...
$actor = PhabricatorUser::getOmnipotentUser();
$source = $this->requireSourceObject();
$item = NuanceItem::initializeNewItem();
$xactions = array();
$xactions[] = id(new NuanceItemTransaction())
->setTransactionType(NuanceItemTransaction::TYPE_SOURCE)
->setNewValue($source->getPHID());
$xactions[] = id(new NuanceItemTransaction())
->setTransactionType(NuanceItemTransaction::TYPE_REQUESTOR)
->setNewValue($requestor->getPHID());
foreach ($properties as $key => $property) {
$xactions[] = id(new NuanceItemTransaction())
->setTransactionType(NuanceItemTransaction::TYPE_PROPERTY)
->setMetadataValue(NuanceItemTransaction::PROPERTY_KEY, $key)
->setNewValue($property);
}
$editor = id(new NuanceItemEditor())
->setActor($actor)
->setActingAsPHID($requestor->getActingAsPHID())
->setContentSource($content_source);
$editor->applyTransactions($item, $xactions);
return $item;
}
/* -( Handling Action Requests )------------------------------------------- */
public function handleActionRequest(AphrontRequest $request) {
return new Aphront404Response();
}
}
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php
index 2dd22a0aa4..1da93bcd38 100644
--- a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php
@@ -1,138 +1,138 @@
<?php
/**
* @task password Managing Encryption Passwords
*/
abstract class PassphraseCredentialType extends Phobject {
abstract public function getCredentialType();
abstract public function getProvidesType();
abstract public function getCredentialTypeName();
abstract public function getCredentialTypeDescription();
abstract public function getSecretLabel();
public function newSecretControl() {
return new AphrontFormTextAreaControl();
}
public static function getAllTypes() {
- $types = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
- return $types;
+ ->setUniqueMethod('getCredentialType')
+ ->execute();
}
public static function getAllCreateableTypes() {
$types = self::getAllTypes();
foreach ($types as $key => $type) {
if (!$type->isCreateable()) {
unset($types[$key]);
}
}
return $types;
}
public static function getAllProvidesTypes() {
$types = array();
foreach (self::getAllTypes() as $type) {
$types[] = $type->getProvidesType();
}
return array_unique($types);
}
public static function getTypeByConstant($constant) {
$all = self::getAllTypes();
$all = mpull($all, null, 'getCredentialType');
return idx($all, $constant);
}
/**
* Can users create new credentials of this type?
*
* @return bool True if new credentials of this type can be created.
*/
public function isCreateable() {
return true;
}
public function didInitializeNewCredential(
PhabricatorUser $actor,
PassphraseCredential $credential) {
return $credential;
}
public function hasPublicKey() {
return false;
}
public function getPublicKey(
PhabricatorUser $viewer,
PassphraseCredential $credential) {
return null;
}
/* -( Passwords )---------------------------------------------------------- */
/**
* Return true to show an additional "Password" field. This is used by
* SSH credentials to strip passwords off private keys.
*
* @return bool True if a password field should be shown to the user.
*
* @task password
*/
public function shouldShowPasswordField() {
return false;
}
/**
* Return the label for the password field, if one is shown.
*
* @return string Human-readable field label.
*
* @task password
*/
public function getPasswordLabel() {
return pht('Password');
}
/**
* Return true if the provided credental requires a password to decrypt.
*
* @param PhutilOpaqueEnvelope Credential secret value.
* @return bool True if the credential needs a password.
*
* @task password
*/
public function requiresPassword(PhutilOpaqueEnvelope $secret) {
return false;
}
/**
* Return the decrypted credential secret, or `null` if the password does
* not decrypt the credential.
*
* @param PhutilOpaqueEnvelope Credential secret value.
* @param PhutilOpaqueEnvelope Credential password.
* @return
* @task password
*/
public function decryptSecret(
PhutilOpaqueEnvelope $secret,
PhutilOpaqueEnvelope $password) {
return $secret;
}
public function shouldRequireUsername() {
return true;
}
}
diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php
index 7006d8bb4a..c69075bdad 100644
--- a/src/applications/phid/type/PhabricatorPHIDType.php
+++ b/src/applications/phid/type/PhabricatorPHIDType.php
@@ -1,239 +1,212 @@
<?php
abstract class PhabricatorPHIDType extends Phobject {
final public function getTypeConstant() {
$class = new ReflectionClass($this);
$const = $class->getConstant('TYPECONST');
if ($const === false) {
throw new Exception(
pht(
'%s class "%s" must define a %s property.',
__CLASS__,
get_class($this),
'TYPECONST'));
}
if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) {
throw new Exception(
pht(
'%s class "%s" has an invalid %s property. PHID '.
'constants must be a four character uppercase string.',
__CLASS__,
get_class($this),
'TYPECONST'));
}
return $const;
}
abstract public function getTypeName();
public function newObject() {
return null;
}
public function getTypeIcon() {
// Default to the application icon if the type doesn't specify one.
$application_class = $this->getPHIDTypeApplicationClass();
if ($application_class) {
$application = newv($application_class, array());
return $application->getFontIcon();
}
return null;
}
/**
* Get the class name for the application this type belongs to.
*
* @return string|null Class name of the corresponding application, or null
* if the type is not bound to an application.
*/
public function getPHIDTypeApplicationClass() {
// TODO: Some day this should probably be abstract, but for now it only
// affects global search and there's no real burning need to go classify
// every PHID type.
return null;
}
/**
* Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type
* by PHID.
*
* If you can not build a single query which satisfies this requirement, you
* can provide a dummy implementation for this method and overload
* @{method:loadObjects} instead.
*
* @param PhabricatorObjectQuery Query being executed.
* @param list<phid> PHIDs to load.
* @return PhabricatorPolicyAwareQuery Query object which loads the
* specified PHIDs when executed.
*/
abstract protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids);
/**
* Load objects of this type, by PHID. For most PHID types, it is only
* necessary to implement @{method:buildQueryForObjects} to get object
* loading to work.
*
* @param PhabricatorObjectQuery Query being executed.
* @param list<phid> PHIDs to load.
* @return list<wild> Corresponding objects.
*/
public function loadObjects(
PhabricatorObjectQuery $query,
array $phids) {
$object_query = $this->buildQueryForObjects($query, $phids)
->setViewer($query->getViewer())
->setParentQuery($query);
// If the user doesn't have permission to use the application at all,
// just mark all the PHIDs as filtered. This primarily makes these
// objects show up as "Restricted" instead of "Unknown" when loaded as
// handles, which is technically true.
if (!$object_query->canViewerUseQueryApplication()) {
$object_query->addPolicyFilteredPHIDs(array_fuse($phids));
return array();
}
return $object_query->execute();
}
/**
* Populate provided handles with application-specific data, like titles and
* URIs.
*
* NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty
* and have the same keys: subclasses are expected to load information only
* for handles with visible objects.
*
* Because of this guarantee, a safe implementation will typically look like*
*
* foreach ($handles as $phid => $handle) {
* $object = $objects[$phid];
*
* $handle->setStuff($object->getStuff());
* // ...
* }
*
* In general, an implementation should call `setName()` and `setURI()` on
* each handle at a minimum. See @{class:PhabricatorObjectHandle} for other
* handle properties.
*
* @param PhabricatorHandleQuery Issuing query object.
* @param list<PhabricatorObjectHandle> Handles to populate with data.
* @param list<Object> Objects for these PHIDs loaded by
* @{method:buildQueryForObjects()}.
* @return void
*/
abstract public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects);
public function canLoadNamedObject($name) {
return false;
}
public function loadNamedObjects(
PhabricatorObjectQuery $query,
array $names) {
throw new PhutilMethodNotImplementedException();
}
/**
* Get all known PHID types.
*
* To get PHID types a given user has access to, see
* @{method:getAllInstalledTypes}.
*
* @return dict<string, PhabricatorPHIDType> Map of type constants to types.
*/
final public static function getAllTypes() {
- static $types;
- if ($types === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- $original = array();
- foreach ($objects as $object) {
- $type = $object->getTypeConstant();
- if (isset($map[$type])) {
- $that_class = $original[$type];
- $this_class = get_class($object);
- throw new Exception(
- pht(
- "Two %s classes (%s, %s) both handle PHID type '%s'. ".
- "A type may be handled by only one class.",
- __CLASS__,
- $that_class,
- $this_class,
- $type));
- }
-
- $original[$type] = get_class($object);
- $map[$type] = $object;
- }
-
- $types = $map;
- }
-
- return $types;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getTypeConstant')
+ ->execute();
}
/**
* Get all PHID types of applications installed for a given viewer.
*
* @param PhabricatorUser Viewing user.
* @return dict<string, PhabricatorPHIDType> Map of constants to installed
* types.
*/
public static function getAllInstalledTypes(PhabricatorUser $viewer) {
$all_types = self::getAllTypes();
$installed_types = array();
$app_classes = array();
foreach ($all_types as $key => $type) {
$app_class = $type->getPHIDTypeApplicationClass();
if ($app_class === null) {
// If the PHID type isn't bound to an application, include it as
// installed.
$installed_types[$key] = $type;
continue;
}
// Otherwise, we need to check if this application is installed before
// including the PHID type.
$app_classes[$app_class][$key] = $type;
}
if ($app_classes) {
$apps = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withInstalled(true)
->withClasses(array_keys($app_classes))
->execute();
foreach ($apps as $app_class => $app) {
$installed_types += $app_classes[$app_class];
}
}
return $installed_types;
}
}
diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php
index 97821a3e18..8e25d134b0 100644
--- a/src/applications/phortune/provider/PhortunePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaymentProvider.php
@@ -1,296 +1,296 @@
<?php
/**
* @task addmethod Adding Payment Methods
*/
abstract class PhortunePaymentProvider extends Phobject {
private $providerConfig;
public function setProviderConfig(
PhortunePaymentProviderConfig $provider_config) {
$this->providerConfig = $provider_config;
return $this;
}
public function getProviderConfig() {
return $this->providerConfig;
}
/**
* Return a short name which identifies this provider.
*/
abstract public function getName();
/* -( Configuring Providers )---------------------------------------------- */
/**
* Return a human-readable provider name for use on the merchant workflow
* where a merchant owner adds providers.
*/
abstract public function getConfigureName();
/**
* Return a human-readable provider description for use on the merchant
* workflow where a merchant owner adds providers.
*/
abstract public function getConfigureDescription();
abstract public function getConfigureInstructions();
abstract public function getConfigureProvidesDescription();
abstract public function getAllConfigurableProperties();
abstract public function getAllConfigurableSecretProperties();
/**
* Read a dictionary of properties from the provider's configuration for
* use when editing the provider.
*/
public function readEditFormValuesFromProviderConfig() {
$properties = $this->getAllConfigurableProperties();
$config = $this->getProviderConfig();
$secrets = $this->getAllConfigurableSecretProperties();
$secrets = array_fuse($secrets);
$map = array();
foreach ($properties as $property) {
$map[$property] = $config->getMetadataValue($property);
if (isset($secrets[$property])) {
$map[$property] = $this->renderConfigurationSecret($map[$property]);
}
}
return $map;
}
/**
* Read a dictionary of properties from a request for use when editing the
* provider.
*/
public function readEditFormValuesFromRequest(AphrontRequest $request) {
$properties = $this->getAllConfigurableProperties();
$map = array();
foreach ($properties as $property) {
$map[$property] = $request->getStr($property);
}
return $map;
}
abstract public function processEditForm(
AphrontRequest $request,
array $values);
abstract public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues);
protected function renderConfigurationSecret($value) {
if (strlen($value)) {
return str_repeat('*', strlen($value));
}
return '';
}
public function isConfigurationSecret($value) {
return preg_match('/^\*+\z/', trim($value));
}
abstract public function canRunConfigurationTest();
public function runConfigurationTest() {
throw new PhortuneNotImplementedException($this);
}
/* -( Selecting Providers )------------------------------------------------ */
public static function getAllProviders() {
- return id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
+ ->execute();
}
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
abstract public function isAcceptingLivePayments();
abstract public function getPaymentMethodDescription();
abstract public function getPaymentMethodIcon();
abstract public function getPaymentMethodProviderDescription();
final public function applyCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
$this->executeCharge($payment_method, $charge);
}
final public function refundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->executeRefund($charge, $refund);
}
abstract protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge);
abstract protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund);
abstract public function updateCharge(PhortuneCharge $charge);
/* -( Adding Payment Methods )--------------------------------------------- */
/**
* @task addmethod
*/
public function canCreatePaymentMethods() {
return false;
}
/**
* @task addmethod
*/
public function translateCreatePaymentMethodErrorCode($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function validateCreatePaymentMethodToken(array $token) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
throw new PhortuneNotImplementedException($this);
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
throw new PhortuneNotImplementedException($this);
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return false;
}
public function renderOneTimePaymentButton(
PhortuneAccount $account,
PhortuneCart $cart,
PhabricatorUser $user) {
require_celerity_resource('phortune-css');
$description = $this->getPaymentMethodProviderDescription();
$details = $this->getPaymentMethodDescription();
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getPaymentMethodIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($description)
->setSubtext($details);
// NOTE: We generate a local URI to make sure the form picks up CSRF tokens.
$uri = $this->getControllerURI(
'checkout',
array(
'cartID' => $cart->getID(),
),
$local = true);
return phabricator_form(
$user,
array(
'action' => $uri,
'method' => 'POST',
),
$button);
}
/* -( Controllers )-------------------------------------------------------- */
final public function getControllerURI(
$action,
array $params = array(),
$local = false) {
$id = $this->getProviderConfig()->getID();
$app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication');
$path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/';
$uri = new PhutilURI($path);
$uri->setQueryParams($params);
if ($local) {
return $uri;
} else {
return PhabricatorEnv::getURI((string)$uri);
}
}
public function canRespondToControllerAction($action) {
return false;
}
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
throw new PhortuneNotImplementedException($this);
}
}
diff --git a/src/applications/policy/capability/PhabricatorPolicyCapability.php b/src/applications/policy/capability/PhabricatorPolicyCapability.php
index a9af1fa00c..b7ff6a060b 100644
--- a/src/applications/policy/capability/PhabricatorPolicyCapability.php
+++ b/src/applications/policy/capability/PhabricatorPolicyCapability.php
@@ -1,94 +1,88 @@
<?php
abstract class PhabricatorPolicyCapability extends Phobject {
const CAN_VIEW = 'view';
const CAN_EDIT = 'edit';
const CAN_JOIN = 'join';
/**
* Get the unique key identifying this capability. This key must be globally
* unique. Application capabilities should be namespaced. For example:
*
* application.create
*
* @return string Globally unique capability key.
*/
final public function getCapabilityKey() {
$class = new ReflectionClass($this);
$const = $class->getConstant('CAPABILITY');
if ($const === false) {
throw new Exception(
pht(
'%s class "%s" must define a %s property.',
__CLASS__,
get_class($this),
'CAPABILITY'));
}
if (!is_string($const)) {
throw new Exception(
pht(
'%s class "%s" has an invalid %s property. '.
'Capability constants must be a string.',
__CLASS__,
get_class($this),
'CAPABILITY'));
}
return $const;
}
/**
* Return a human-readable descriptive name for this capability, like
* "Can View".
*
* @return string Human-readable name describing the capability.
*/
abstract public function getCapabilityName();
/**
* Return a human-readable string describing what not having this capability
* prevents the user from doing. For example:
*
* - You do not have permission to edit this object.
* - You do not have permission to create new tasks.
*
* @return string Human-readable name describing what failing a check for this
* capability prevents the user from doing.
*/
public function describeCapabilityRejection() {
return null;
}
/**
* Can this capability be set to "public"? Broadly, this is only appropriate
* for view and view-related policies.
*
* @return bool True to allow the "public" policy. Returns false by default.
*/
public function shouldAllowPublicPolicySetting() {
return false;
}
final public static function getCapabilityByKey($key) {
return idx(self::getCapabilityMap(), $key);
}
final public static function getCapabilityMap() {
- static $map;
- if ($map === null) {
- $capabilities = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = mpull($capabilities, null, 'getCapabilityKey');
- }
-
- return $map;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getCapabilityKey')
+ ->execute();
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 90123ee7c1..43b6d72075 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1137 +1,1135 @@
<?php
/**
* Represents an abstract search engine for an application. It supports
* creating and storing saved queries.
*
* @task construct Constructing Engines
* @task app Applications
* @task builtin Builtin Queries
* @task uri Query URIs
* @task dates Date Filters
* @task order Result Ordering
* @task read Reading Utilities
* @task exec Paging and Executing Queries
* @task render Rendering Results
*/
abstract class PhabricatorApplicationSearchEngine extends Phobject {
private $application;
private $viewer;
private $errors = array();
private $customFields = false;
private $request;
private $context;
const CONTEXT_LIST = 'list';
const CONTEXT_PANEL = 'panel';
public function newResultObject() {
// We may be able to get this automatically if newQuery() is implemented.
$query = $this->newQuery();
if ($query) {
$object = $query->newResultObject();
if ($object) {
return $object;
}
}
return null;
}
public function newQuery() {
return null;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
protected function requireViewer() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
return $this->viewer;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function isPanelContext() {
return ($this->context == self::CONTEXT_PANEL);
}
public function canUseInPanelContext() {
return true;
}
public function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
/**
* Create a saved query object from the request.
*
* @param AphrontRequest The search request.
* @return PhabricatorSavedQuery
*/
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$saved = new PhabricatorSavedQuery();
foreach ($fields as $field) {
$field->setViewer($viewer);
$value = $field->readValueFromRequest($request);
$saved->setParameter($field->getKey(), $value);
}
return $saved;
}
/**
* Executes the saved query.
*
* @param PhabricatorSavedQuery The saved query to operate on.
* @return The result of the query.
*/
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$saved = clone $saved;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$map = array();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
$value = $field->getValueForQuery($field->getValue());
$map[$field->getKey()] = $value;
}
$query = $this->buildQueryFromParameters($map);
$object = $this->newResultObject();
if (!$object) {
return $query;
}
if ($object instanceof PhabricatorSubscribableInterface) {
if (!empty($map['subscriberPHIDs'])) {
$query->withEdgeLogicPHIDs(
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$map['subscriberPHIDs']);
}
}
if ($object instanceof PhabricatorProjectInterface) {
if (!empty($map['projectPHIDs'])) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$map['projectPHIDs']);
}
}
if ($object instanceof PhabricatorSpacesInterface) {
if (!empty($map['spacePHIDs'])) {
$query->withSpacePHIDs($map['spacePHIDs']);
} else {
// If the user doesn't search for objects in specific spaces, we
// default to "all active spaces you have permission to view".
$query->withSpaceIsArchived(false);
}
}
if ($object instanceof PhabricatorCustomFieldInterface) {
$this->applyCustomFieldsToQuery($query, $saved);
}
$order = $saved->getParameter('order');
$builtin = $query->getBuiltinOrderAliasMap();
if (strlen($order) && isset($builtin[$order])) {
$query->setOrder($order);
} else {
// If the order is invalid or not available, we choose the first
// builtin order. This isn't always the default order for the query,
// but is the first value in the "Order" dropdown, and makes the query
// behavior more consistent with the UI. In queries where the two
// orders differ, this order is the preferred order for humans.
$query->setOrder(head_key($builtin));
}
return $query;
}
/**
* Hook for subclasses to adjust saved queries prior to use.
*
* If an application changes how queries are saved, it can implement this
* hook to keep old queries working the way users expect, by reading,
* adjusting, and overwriting parameters.
*
* @param PhabricatorSavedQuery Saved query which will be executed.
* @return void
*/
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
return;
}
protected function buildQueryFromParameters(array $parameters) {
throw new PhutilMethodNotImplementedException();
}
/**
* Builds the search form using the request.
*
* @param AphrontFormView Form to populate.
* @param PhabricatorSavedQuery The query from which to build the form.
* @return void
*/
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$saved = clone $saved;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$fields = $this->adjustFieldsForDisplay($fields);
$viewer = $this->requireViewer();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
}
foreach ($fields as $field) {
foreach ($field->getErrors() as $error) {
$this->addError(last($error));
}
}
foreach ($fields as $field) {
$field->appendToForm($form);
}
}
protected function buildSearchFields() {
$fields = array();
foreach ($this->buildCustomSearchFields() as $field) {
$fields[] = $field;
}
$object = $this->newResultObject();
if ($object) {
if ($object instanceof PhabricatorSubscribableInterface) {
$fields[] = id(new PhabricatorSearchSubscribersField())
->setLabel(pht('Subscribers'))
->setKey('subscriberPHIDs')
->setAliases(array('subscriber', 'subscribers'));
}
if ($object instanceof PhabricatorProjectInterface) {
$fields[] = id(new PhabricatorProjectSearchField())
->setKey('projectPHIDs')
->setAliases(array('project', 'projects'))
->setLabel(pht('Projects'));
}
if ($object instanceof PhabricatorSpacesInterface) {
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
$fields[] = id(new PhabricatorSpacesSearchField())
->setKey('spacePHIDs')
->setAliases(array('space', 'spaces'))
->setLabel(pht('Spaces'));
}
}
}
foreach ($this->buildCustomFieldSearchFields() as $custom_field) {
$fields[] = $custom_field;
}
$query = $this->newQuery();
if ($query && $this->shouldShowOrderField()) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$fields[] = id(new PhabricatorSearchOrderField())
->setLabel(pht('Order By'))
->setKey('order')
->setOrderAliases($query->getBuiltinOrderAliasMap())
->setOptions($orders);
}
$field_map = array();
foreach ($fields as $field) {
$key = $field->getKey();
if (isset($field_map[$key])) {
throw new Exception(
pht(
'Two fields in this SearchEngine use the same key ("%s"), but '.
'each field must use a unique key.',
$key));
}
$field_map[$key] = $field;
}
return $field_map;
}
protected function shouldShowOrderField() {
return true;
}
private function adjustFieldsForDisplay(array $field_map) {
$order = $this->getDefaultFieldOrder();
$head_keys = array();
$tail_keys = array();
$seen_tail = false;
foreach ($order as $order_key) {
if ($order_key === '...') {
$seen_tail = true;
continue;
}
if (!$seen_tail) {
$head_keys[] = $order_key;
} else {
$tail_keys[] = $order_key;
}
}
$head = array_select_keys($field_map, $head_keys);
$body = array_diff_key($field_map, array_fuse($tail_keys));
$tail = array_select_keys($field_map, $tail_keys);
$result = $head + $body + $tail;
foreach ($this->getHiddenFields() as $hidden_key) {
unset($result[$hidden_key]);
}
return $result;
}
protected function buildCustomSearchFields() {
throw new PhutilMethodNotImplementedException();
}
/**
* Define the default display order for fields by returning a list of
* field keys.
*
* You can use the special key `...` to mean "all unspecified fields go
* here". This lets you easily put important fields at the top of the form,
* standard fields in the middle of the form, and less important fields at
* the bottom.
*
* For example, you might return a list like this:
*
* return array(
* 'authorPHIDs',
* 'reviewerPHIDs',
* '...',
* 'createdAfter',
* 'createdBefore',
* );
*
* Any unspecified fields (including custom fields and fields added
* automatically by infrastruture) will be put in the middle.
*
* @return list<string> Default ordering for field keys.
*/
protected function getDefaultFieldOrder() {
return array();
}
/**
* Return a list of field keys which should be hidden from the viewer.
*
* @return list<string> Fields to hide.
*/
protected function getHiddenFields() {
return array();
}
public function getErrors() {
return $this->errors;
}
public function addError($error) {
$this->errors[] = $error;
return $this;
}
/**
* Return an application URI corresponding to the results page of a query.
* Normally, this is something like `/application/query/QUERYKEY/`.
*
* @param string The query key to build a URI for.
* @return string URI where the query can be executed.
* @task uri
*/
public function getQueryResultsPageURI($query_key) {
return $this->getURI('query/'.$query_key.'/');
}
/**
* Return an application URI for query management. This is used when, e.g.,
* a query deletion operation is cancelled.
*
* @return string URI where queries can be managed.
* @task uri
*/
public function getQueryManagementURI() {
return $this->getURI('query/edit/');
}
/**
* Return the URI to a path within the application. Used to construct default
* URIs for management and results.
*
* @return string URI to path.
* @task uri
*/
abstract protected function getURI($path);
/**
* Return a human readable description of the type of objects this query
* searches for.
*
* For example, "Tasks" or "Commits".
*
* @return string Human-readable description of what this engine is used to
* find.
*/
abstract public function getResultTypeDescription();
public function newSavedQuery() {
return id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($this));
}
public function addNavigationItems(PHUIListView $menu) {
$viewer = $this->requireViewer();
$menu->newLabel(pht('Queries'));
$named_queries = $this->loadEnabledNamedQueries();
foreach ($named_queries as $query) {
$key = $query->getQueryKey();
$uri = $this->getQueryResultsPageURI($key);
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
}
if ($viewer->isLoggedIn()) {
$manage_uri = $this->getQueryManagementURI();
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
}
$menu->newLabel(pht('Search'));
$advanced_uri = $this->getQueryResultsPageURI('advanced');
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withEngineClassNames(array(get_class($this)))
->execute();
$named_queries = mpull($named_queries, null, 'getQueryKey');
$builtin = $this->getBuiltinQueries($viewer);
$builtin = mpull($builtin, null, 'getQueryKey');
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin()) {
if (isset($builtin[$key])) {
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
unset($builtin[$key]);
} else {
unset($named_queries[$key]);
}
}
unset($builtin[$key]);
}
$named_queries = msort($named_queries, 'getSortKey');
return $named_queries + $builtin;
}
public function loadEnabledNamedQueries() {
$named_queries = $this->loadAllNamedQueries();
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
unset($named_queries[$key]);
}
}
return $named_queries;
}
protected function setQueryProjects(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($this->requireViewer());
$projects = $saved->getParameter('projects', array());
$constraints = $datasource->evaluateTokens($projects);
if ($constraints) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
}
return $this;
}
/* -( Applications )------------------------------------------------------- */
protected function getApplicationURI($path = '') {
return $this->getApplication()->getApplicationURI($path);
}
protected function getApplication() {
if (!$this->application) {
$class = $this->getApplicationClassName();
$this->application = id(new PhabricatorApplicationQuery())
->setViewer($this->requireViewer())
->withClasses(array($class))
->withInstalled(true)
->executeOne();
if (!$this->application) {
throw new Exception(
pht(
'Application "%s" is not installed!',
$class));
}
}
return $this->application;
}
abstract public function getApplicationClassName();
/* -( Constructing Engines )----------------------------------------------- */
/**
* Load all available application search engines.
*
* @return list<PhabricatorApplicationSearchEngine> All available engines.
* @task construct
*/
public static function getAllEngines() {
- $engines = id(new PhutilSymbolLoader())
+ return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- return $engines;
+ ->execute();
}
/**
* Get an engine by class name, if it exists.
*
* @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
* not exist.
* @task construct
*/
public static function getEngineByClassName($class_name) {
return idx(self::getAllEngines(), $class_name);
}
/* -( Builtin Queries )---------------------------------------------------- */
/**
* @task builtin
*/
public function getBuiltinQueries() {
$names = $this->getBuiltinQueryNames();
$queries = array();
$sequence = 0;
foreach ($names as $key => $name) {
$queries[$key] = id(new PhabricatorNamedQuery())
->setUserPHID($this->requireViewer()->getPHID())
->setEngineClassName(get_class($this))
->setQueryName($name)
->setQueryKey($key)
->setSequence((1 << 24) + $sequence++)
->setIsBuiltin(true);
}
return $queries;
}
/**
* @task builtin
*/
public function getBuiltinQuery($query_key) {
if (!$this->isBuiltinQuery($query_key)) {
throw new Exception(pht("'%s' is not a builtin!", $query_key));
}
return idx($this->getBuiltinQueries(), $query_key);
}
/**
* @task builtin
*/
protected function getBuiltinQueryNames() {
return array();
}
/**
* @task builtin
*/
public function isBuiltinQuery($query_key) {
$builtins = $this->getBuiltinQueries();
return isset($builtins[$query_key]);
}
/**
* @task builtin
*/
public function buildSavedQueryFromBuiltin($query_key) {
throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
}
/* -( Reading Utilities )--------------------------------------------------- */
/**
* Read a list of user PHIDs from a request in a flexible way. This method
* supports either of these forms:
*
* users[]=alincoln&users[]=htaft
* users=alincoln,htaft
*
* Additionally, users can be specified either by PHID or by name.
*
* The main goal of this flexibility is to allow external programs to generate
* links to pages (like "alincoln's open revisions") without needing to make
* API calls.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @param list<const> Other permitted PHID types.
* @return list<phid> List of user PHIDs and selector functions.
* @task read
*/
protected function readUsersFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$names = array();
$allow_types = array_fuse($allow_types);
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $user_type) {
$phids[] = $item;
} else if (isset($allow_types[$type])) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$names[] = $item;
}
}
}
if ($names) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireViewer())
->withUsernames($names)
->execute();
foreach ($users as $user) {
$phids[] = $user->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of project PHIDs from a request in a flexible way.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of projet PHIDs and selector functions.
* @task read
*/
protected function readProjectsFromRequest(AphrontRequest $request, $key) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$slugs = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $project_type) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$slugs[] = $item;
}
}
}
if ($slugs) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireViewer())
->withSlugs($slugs)
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of subscribers from a request in a flexible way.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of object PHIDs.
* @task read
*/
protected function readSubscribersFromRequest(
AphrontRequest $request,
$key) {
return $this->readUsersFromRequest(
$request,
$key,
array(
PhabricatorProjectProjectPHIDType::TYPECONST,
));
}
/**
* Read a list of generic PHIDs from a request in a flexible way. Like
* @{method:readUsersFromRequest}, this method supports either array or
* comma-delimited forms. Objects can be specified either by PHID or by
* object name.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @param list<const> Optional, list of permitted PHID types.
* @return list<phid> List of object PHIDs.
*
* @task read
*/
protected function readPHIDsFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->requireViewer())
->withNames($list)
->execute();
$list = mpull($objects, 'getPHID');
if (!$list) {
return array();
}
// If only certain PHID types are allowed, filter out all the others.
if ($allow_types) {
$allow_types = array_fuse($allow_types);
foreach ($list as $key => $phid) {
if (empty($allow_types[phid_get_type($phid)])) {
unset($list[$key]);
}
}
}
return $list;
}
/**
* Read a list of items from the request, in either array format or string
* format:
*
* list[]=item1&list[]=item2
* list=item1,item2
*
* This provides flexibility when constructing URIs, especially from external
* sources.
*
* @param AphrontRequest Request to read strings from.
* @param string Key to read in the request.
* @return list<string> List of values.
*/
protected function readListFromRequest(
AphrontRequest $request,
$key) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
if (!$list) {
return array();
}
return $list;
}
protected function readDateFromRequest(
AphrontRequest $request,
$key) {
$value = AphrontFormDateControlValue::newFromRequest($request, $key);
if ($value->isEmpty()) {
return null;
}
return $value->getDictionary();
}
protected function readBoolFromRequest(
AphrontRequest $request,
$key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
/* -( Dates )-------------------------------------------------------------- */
/**
* @task dates
*/
protected function parseDateTime($date_time) {
if (!strlen($date_time)) {
return null;
}
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
}
/**
* @task dates
*/
protected function buildDateRange(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query,
$start_key,
$start_name,
$end_key,
$end_name) {
$start_str = $saved_query->getParameter($start_key);
$start = null;
if (strlen($start_str)) {
$start = $this->parseDateTime($start_str);
if (!$start) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$start_name));
}
}
$end_str = $saved_query->getParameter($end_key);
$end = null;
if (strlen($end_str)) {
$end = $this->parseDateTime($end_str);
if (!$end) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$end_name));
}
}
if ($start && $end && ($start >= $end)) {
$this->addError(
pht(
'"%s" must be a date before "%s".',
$start_name,
$end_name));
}
$form
->appendChild(
id(new PHUIFormFreeformDateControl())
->setName($start_key)
->setLabel($start_name)
->setValue($start_str))
->appendChild(
id(new AphrontFormTextControl())
->setName($end_key)
->setLabel($end_name)
->setValue($end_str));
}
/* -( Paging and Executing Queries )--------------------------------------- */
public function getPageSize(PhabricatorSavedQuery $saved) {
$limit = (int)$saved->getParameter('limit');
if ($limit > 0) {
return $limit;
}
return 100;
}
public function shouldUseOffsetPaging() {
return false;
}
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
if ($this->shouldUseOffsetPaging()) {
$pager = new PHUIPagerView();
} else {
$pager = new AphrontCursorPagerView();
}
$page_size = $this->getPageSize($saved);
if (is_finite($page_size)) {
$pager->setPageSize($page_size);
} else {
// Consider an INF pagesize to mean a large finite pagesize.
// TODO: It would be nice to handle this more gracefully, but math
// with INF seems to vary across PHP versions, systems, and runtimes.
$pager->setPageSize(0xFFFF);
}
return $pager;
}
public function executeQuery(
PhabricatorPolicyAwareQuery $query,
AphrontView $pager) {
$query->setViewer($this->requireViewer());
if ($this->shouldUseOffsetPaging()) {
$objects = $query->executeWithOffsetPager($pager);
} else {
$objects = $query->executeWithCursorPager($pager);
}
return $objects;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function renderResults(
array $objects,
PhabricatorSavedQuery $query) {
$phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
if ($phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->witHPHIDs($phids)
->execute();
} else {
$handles = array();
}
return $this->renderResultList($objects, $query, $handles);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
return array();
}
protected function renderResultList(
array $objects,
PhabricatorSavedQuery $query,
array $handles) {
throw new Exception(pht('Not supported here yet!'));
}
/* -( Application Search )------------------------------------------------- */
/**
* Retrieve an object to use to define custom fields for this search.
*
* To integrate with custom fields, subclasses should override this method
* and return an instance of the application object which implements
* @{interface:PhabricatorCustomFieldInterface}.
*
* @return PhabricatorCustomFieldInterface|null Object with custom fields.
* @task appsearch
*/
public function getCustomFieldObject() {
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
return $object;
}
return null;
}
/**
* Get the custom fields for this search.
*
* @return PhabricatorCustomFieldList|null Custom fields, if this search
* supports custom fields.
* @task appsearch
*/
public function getCustomFieldList() {
if ($this->customFields === false) {
$object = $this->getCustomFieldObject();
if ($object) {
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->requireViewer());
} else {
$fields = null;
}
$this->customFields = $fields;
}
return $this->customFields;
}
/**
* Applies data from a saved query to an executable query.
*
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param PhabricatorSavedQuery Saved query to read.
* @return void
*/
protected function applyCustomFieldsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$value = $field->applyApplicationSearchConstraintToQuery(
$this,
$query,
$saved->getParameter('custom:'.$field->getFieldIndex()));
}
}
private function buildCustomFieldSearchFields() {
$list = $this->getCustomFieldList();
if (!$list) {
return array();
}
$fields = array();
foreach ($list->getFields() as $field) {
$fields[] = id(new PhabricatorSearchCustomFieldProxyField())
->setSearchEngine($this)
->setCustomField($field);
}
return $fields;
}
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngine.php b/src/applications/search/engine/PhabricatorSearchEngine.php
index 431d447927..62a574465f 100644
--- a/src/applications/search/engine/PhabricatorSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorSearchEngine.php
@@ -1,162 +1,138 @@
<?php
/**
* Base class for Phabricator search engine providers. Each engine must offer
* three capabilities: indexing, searching, and reconstruction (this can be
* stubbed out if an engine can't reasonably do it, it is used for debugging).
*/
abstract class PhabricatorSearchEngine extends Phobject {
/* -( Engine Metadata )---------------------------------------------------- */
/**
* Return a unique, nonempty string which identifies this storage engine.
*
* @return string Unique string for this engine, max length 32.
* @task meta
*/
abstract public function getEngineIdentifier();
/**
* Prioritize this engine relative to other engines.
*
* Engines with a smaller priority number get an opportunity to write files
* first. Generally, lower-latency filestores should have lower priority
* numbers, and higher-latency filestores should have higher priority
* numbers. Setting priority to approximately the number of milliseconds of
* read latency will generally produce reasonable results.
*
* In conjunction with filesize limits, the goal is to store small files like
* profile images, thumbnails, and text snippets in lower-latency engines,
* and store large files in higher-capacity engines.
*
* @return float Engine priority.
* @task meta
*/
abstract public function getEnginePriority();
/**
* Return `true` if the engine is currently writable.
*
* Engines that are disabled or missing configuration should return `false`
* to prevent new writes. If writes were made with this engine in the past,
* the application may still try to perform reads.
*
* @return bool True if this engine can support new writes.
* @task meta
*/
abstract public function isEnabled();
/* -( Managing Documents )------------------------------------------------- */
/**
* Update the index for an abstract document.
*
* @param PhabricatorSearchAbstractDocument Document to update.
* @return void
*/
abstract public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $document);
/**
* Reconstruct the document for a given PHID. This is used for debugging
* and does not need to be perfect if it is unreasonable to implement it.
*
* @param phid Document PHID to reconstruct.
* @return PhabricatorSearchAbstractDocument Abstract document.
*/
abstract public function reconstructDocument($phid);
/**
* Execute a search query.
*
* @param PhabricatorSavedQuery A query to execute.
* @return list A list of matching PHIDs.
*/
abstract public function executeSearch(PhabricatorSavedQuery $query);
/**
* Does the search index exist?
*
* @return bool
*/
abstract public function indexExists();
/**
* Is the index in a usable state?
*
* @return bool
*/
public function indexIsSane() {
return $this->indexExists();
}
/**
* Do any sort of setup for the search index.
*
* @return void
*/
public function initIndex() {}
/* -( Loading Storage Engines )-------------------------------------------- */
/**
* @task load
*/
public static function loadAllEngines() {
- static $engines;
-
- if ($engines === null) {
- $objects = id(new PhutilSymbolLoader())
- ->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
- foreach ($objects as $engine) {
- $key = $engine->getEngineIdentifier();
- if (empty($map[$key])) {
- $map[$key] = $engine;
- } else {
- throw new Exception(
- pht(
- 'Search engines "%s" and "%s" have the same engine identifier '.
- '"%s". Each storage engine must have a unique identifier.',
- get_class($engine),
- get_class($map[$key]),
- $key));
- }
- }
-
- $map = msort($map, 'getEnginePriority');
-
- $engines = $map;
- }
-
- return $engines;
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getEngineIdentifier')
+ ->setSortMethod('getEnginePriority')
+ ->execute();
}
/**
* @task load
*/
public static function loadActiveEngines() {
$engines = self::loadAllEngines();
$active = array();
foreach ($engines as $key => $engine) {
if (!$engine->isEnabled()) {
continue;
}
$active[$key] = $engine;
}
return $active;
}
public static function loadEngine() {
return head(self::loadActiveEngines());
}
}
diff --git a/src/infrastructure/edges/type/PhabricatorEdgeType.php b/src/infrastructure/edges/type/PhabricatorEdgeType.php
index d62c5a9670..c86af5c027 100644
--- a/src/infrastructure/edges/type/PhabricatorEdgeType.php
+++ b/src/infrastructure/edges/type/PhabricatorEdgeType.php
@@ -1,236 +1,219 @@
<?php
/**
* Defines an edge type.
*
* Edges are typed, directed connections between two objects. They are used to
* represent most simple relationships, like when a user is subscribed to an
* object or an object is a member of a project.
*
* @task load Loading Types
*/
abstract class PhabricatorEdgeType extends Phobject {
final public function getEdgeConstant() {
$class = new ReflectionClass($this);
$const = $class->getConstant('EDGECONST');
if ($const === false) {
throw new Exception(
pht(
'%s class "%s" must define an %s property.',
__CLASS__,
get_class($this),
'EDGECONST'));
}
if (!is_int($const) || ($const <= 0)) {
throw new Exception(
pht(
'%s class "%s" has an invalid %s property. '.
'Edge constants must be positive integers.',
__CLASS__,
get_class($this),
'EDGECONST'));
}
return $const;
}
public function getInverseEdgeConstant() {
return null;
}
public function shouldPreventCycles() {
return false;
}
public function shouldWriteInverseTransactions() {
return false;
}
public function getTransactionPreviewString($actor) {
return pht(
'%s edited edge metadata.',
$actor);
}
public function getTransactionAddString(
$actor,
$add_count,
$add_edges) {
return pht(
'%s added %s edge(s): %s.',
$actor,
$add_count,
$add_edges);
}
public function getTransactionRemoveString(
$actor,
$rem_count,
$rem_edges) {
return pht(
'%s removed %s edge(s): %s.',
$actor,
$rem_count,
$rem_edges);
}
public function getTransactionEditString(
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
'%s edited %s edge(s), added %s: %s; removed %s: %s.',
$actor,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
public function getFeedAddString(
$actor,
$object,
$add_count,
$add_edges) {
return pht(
'%s added %s edge(s) to %s: %s.',
$actor,
$add_count,
$object,
$add_edges);
}
public function getFeedRemoveString(
$actor,
$object,
$rem_count,
$rem_edges) {
return pht(
'%s removed %s edge(s) from %s: %s.',
$actor,
$rem_count,
$object,
$rem_edges);
}
public function getFeedEditString(
$actor,
$object,
$total_count,
$add_count,
$add_edges,
$rem_count,
$rem_edges) {
return pht(
'%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.',
$actor,
$total_count,
$object,
$add_count,
$add_edges,
$rem_count,
$rem_edges);
}
/* -( Loading Types )------------------------------------------------------ */
/**
* @task load
*/
public static function getAllTypes() {
static $type_map;
if ($type_map === null) {
- $types = id(new PhutilSymbolLoader())
+ $types = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
-
- $map = array();
-
- foreach ($types as $class => $type) {
- $const = $type->getEdgeConstant();
-
- if (isset($map[$const])) {
- throw new Exception(
- pht(
- 'Two edge types ("%s", "%s") share the same edge constant '.
- '(%d). Each edge type must have a unique constant.',
- $class,
- get_class($map[$const]),
- $const));
- }
-
- $map[$const] = $type;
- }
+ ->setUniqueMethod('getEdgeConstant')
+ ->execute();
// Check that all the inverse edge definitions actually make sense. If
// edge type A says B is its inverse, B must exist and say that A is its
// inverse.
- foreach ($map as $const => $type) {
+ foreach ($types as $const => $type) {
$inverse = $type->getInverseEdgeConstant();
if ($inverse === null) {
continue;
}
- if (empty($map[$inverse])) {
+ if (empty($types[$inverse])) {
throw new Exception(
pht(
'Edge type "%s" ("%d") defines an inverse type ("%d") which '.
'does not exist.',
get_class($type),
$const,
$inverse));
}
- $inverse_inverse = $map[$inverse]->getInverseEdgeConstant();
+ $inverse_inverse = $types[$inverse]->getInverseEdgeConstant();
if ($inverse_inverse !== $const) {
throw new Exception(
pht(
'Edge type "%s" ("%d") defines an inverse type ("%d"), but that '.
'inverse type defines a different type ("%d") as its '.
'inverse.',
get_class($type),
$const,
$inverse,
$inverse_inverse));
}
}
- $type_map = $map;
+ $type_map = $types;
}
return $type_map;
}
/**
* @task load
*/
public static function getByConstant($const) {
$type = idx(self::getAllTypes(), $const);
if (!$type) {
throw new Exception(
pht('Unknown edge constant "%s"!', $const));
}
return $type;
}
}
diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
index 3b136b4ec3..ef8c208b9f 100644
--- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php
+++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php
@@ -1,431 +1,420 @@
<?php
/**
* Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt",
* "scrypt", etc.
*
* Hashers define suitability and strength, and the system automatically
* chooses the strongest available hasher and can prompt users to upgrade as
* soon as a stronger hasher is available.
*
* @task hasher Implementing a Hasher
* @task hashing Using Hashers
*/
abstract class PhabricatorPasswordHasher extends Phobject {
const MAXIMUM_STORAGE_SIZE = 128;
/* -( Implementing a Hasher )---------------------------------------------- */
/**
* Return a human-readable description of this hasher, like "Iterated MD5".
*
* @return string Human readable hash name.
* @task hasher
*/
abstract public function getHumanReadableName();
/**
* Return a short, unique, key identifying this hasher, like "md5" or
* "bcrypt". This identifier should not be translated.
*
* @return string Short, unique hash name.
* @task hasher
*/
abstract public function getHashName();
/**
* Return the maximum byte length of hashes produced by this hasher. This is
* used to prevent storage overflows.
*
* @return int Maximum number of bytes in hashes this class produces.
* @task hasher
*/
abstract public function getHashLength();
/**
* Return `true` to indicate that any required extensions or dependencies
* are available, and this hasher is able to perform hashing.
*
* @return bool True if this hasher can execute.
* @task hasher
*/
abstract public function canHashPasswords();
/**
* Return a human-readable string describing why this hasher is unable
* to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.".
*
* @return string Human-readable description of how to enable this hasher.
* @task hasher
*/
abstract public function getInstallInstructions();
/**
* Return an indicator of this hasher's strength. When choosing to hash
* new passwords, the strongest available hasher which is usuable for new
* passwords will be used, and the presence of a stronger hasher will
* prompt users to update their hashes.
*
* Generally, this method should return a larger number than hashers it is
* preferable to, but a smaller number than hashers which are better than it
* is. This number does not need to correspond directly with the actual hash
* strength.
*
* @return float Strength of this hasher.
* @task hasher
*/
abstract public function getStrength();
/**
* Return a short human-readable indicator of this hasher's strength, like
* "Weak", "Okay", or "Good".
*
* This is only used to help administrators make decisions about
* configuration.
*
* @return string Short human-readable description of hash strength.
* @task hasher
*/
abstract public function getHumanReadableStrength();
/**
* Produce a password hash.
*
* @param PhutilOpaqueEnvelope Text to be hashed.
* @return PhutilOpaqueEnvelope Hashed text.
* @task hasher
*/
abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope);
/**
* Verify that a password matches a hash.
*
* The default implementation checks for equality; if a hasher embeds salt in
* hashes it should override this method and perform a salt-aware comparison.
*
* @param PhutilOpaqueEnvelope Password to compare.
* @param PhutilOpaqueEnvelope Bare password hash.
* @return bool True if the passwords match.
* @task hasher
*/
protected function verifyPassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $hash) {
$actual_hash = $this->getPasswordHash($password)->openEnvelope();
$expect_hash = $hash->openEnvelope();
return ($actual_hash === $expect_hash);
}
/**
* Check if an existing hash created by this algorithm is upgradeable.
*
* The default implementation returns `false`. However, hash algorithms which
* have (for example) an internal cost function may be able to upgrade an
* existing hash to a stronger one with a higher cost.
*
* @param PhutilOpaqueEnvelope Bare hash.
* @return bool True if the hash can be upgraded without
* changing the algorithm (for example, to a
* higher cost).
* @task hasher
*/
protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) {
return false;
}
/* -( Using Hashers )------------------------------------------------------ */
/**
* Get the hash of a password for storage.
*
* @param PhutilOpaqueEnvelope Password text.
* @return PhutilOpaqueEnvelope Hashed text.
* @task hashing
*/
final public function getPasswordHashForStorage(
PhutilOpaqueEnvelope $envelope) {
$name = $this->getHashName();
$hash = $this->getPasswordHash($envelope);
$actual_len = strlen($hash->openEnvelope());
$expect_len = $this->getHashLength();
if ($actual_len > $expect_len) {
throw new Exception(
pht(
"Password hash '%s' produced a hash of length %d, but a ".
"maximum length of %d was expected.",
$name,
new PhutilNumber($actual_len),
new PhutilNumber($expect_len)));
}
return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope());
}
/**
* Parse a storage hash into its components, like the hash type and hash
* data.
*
* @return map Dictionary of information about the hash.
* @task hashing
*/
private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) {
$raw_hash = $hash->openEnvelope();
if (strpos($raw_hash, ':') === false) {
throw new Exception(
pht(
'Malformed password hash, expected "name:hash".'));
}
list($name, $hash) = explode(':', $raw_hash);
return array(
'name' => $name,
'hash' => new PhutilOpaqueEnvelope($hash),
);
}
/**
* Get all available password hashers. This may include hashers which can not
* actually be used (for example, a required extension is missing).
*
* @return list<PhabicatorPasswordHasher> Hasher objects.
* @task hashing
*/
public static function getAllHashers() {
- $objects = id(new PhutilSymbolLoader())
+ $objects = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
- ->loadObjects();
+ ->setUniqueMethod('getHashName')
+ ->execute();
- $map = array();
foreach ($objects as $object) {
$name = $object->getHashName();
$potential_length = strlen($name) + $object->getHashLength() + 1;
$maximum_length = self::MAXIMUM_STORAGE_SIZE;
if ($potential_length > $maximum_length) {
throw new Exception(
pht(
'Hasher "%s" may produce hashes which are too long to fit in '.
'storage. %d characters are available, but its hashes may be '.
'up to %d characters in length.',
$name,
$maximum_length,
$potential_length));
}
-
- if (isset($map[$name])) {
- throw new Exception(
- pht(
- 'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '.
- 'hasher must have a unique name.',
- $name,
- get_class($object),
- get_class($map[$name])));
- }
- $map[$name] = $object;
}
- return $map;
+ return $objects;
}
/**
* Get all usable password hashers. This may include hashers which are
* not desirable or advisable.
*
* @return list<PhabicatorPasswordHasher> Hasher objects.
* @task hashing
*/
public static function getAllUsableHashers() {
$hashers = self::getAllHashers();
foreach ($hashers as $key => $hasher) {
if (!$hasher->canHashPasswords()) {
unset($hashers[$key]);
}
}
return $hashers;
}
/**
* Get the best (strongest) available hasher.
*
* @return PhabicatorPasswordHasher Best hasher.
* @task hashing
*/
public static function getBestHasher() {
$hashers = self::getAllUsableHashers();
$hashers = msort($hashers, 'getStrength');
$hasher = last($hashers);
if (!$hasher) {
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'There are no password hashers available which are usable for '.
'new passwords.'));
}
return $hasher;
}
/**
* Get the hashser for a given stored hash.
*
* @return PhabicatorPasswordHasher Corresponding hasher.
* @task hashing
*/
public static function getHasherForHash(PhutilOpaqueEnvelope $hash) {
$info = self::parseHashFromStorage($hash);
$name = $info['name'];
$usable = self::getAllUsableHashers();
if (isset($usable[$name])) {
return $usable[$name];
}
$all = self::getAllHashers();
if (isset($all[$name])) {
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'Attempting to compare a password saved with the "%s" hash. The '.
'hasher exists, but is not currently usable. %s',
$name,
$all[$name]->getInstallInstructions()));
}
throw new PhabricatorPasswordHasherUnavailableException(
pht(
'Attempting to compare a password saved with the "%s" hash. No such '.
'hasher is known to Phabricator.',
$name));
}
/**
* Test if a password is using an weaker hash than the strongest available
* hash. This can be used to prompt users to upgrade, or automatically upgrade
* on login.
*
* @return bool True to indicate that rehashing this password will improve
* the hash strength.
* @task hashing
*/
public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) {
if (!strlen($hash->openEnvelope())) {
throw new Exception(
pht('Expected a password hash, received nothing!'));
}
$current_hasher = self::getHasherForHash($hash);
$best_hasher = self::getBestHasher();
if ($current_hasher->getHashName() != $best_hasher->getHashName()) {
// If the algorithm isn't the best one, we can upgrade.
return true;
}
$info = self::parseHashFromStorage($hash);
if ($current_hasher->canUpgradeInternalHash($info['hash'])) {
// If the algorithm provides an internal upgrade, we can also upgrade.
return true;
}
// Already on the best algorithm with the best settings.
return false;
}
/**
* Generate a new hash for a password, using the best available hasher.
*
* @param PhutilOpaqueEnvelope Password to hash.
* @return PhutilOpaqueEnvelope Hashed password, using best available
* hasher.
* @task hashing
*/
public static function generateNewPasswordHash(
PhutilOpaqueEnvelope $password) {
$hasher = self::getBestHasher();
return $hasher->getPasswordHashForStorage($password);
}
/**
* Compare a password to a stored hash.
*
* @param PhutilOpaqueEnvelope Password to compare.
* @param PhutilOpaqueEnvelope Stored password hash.
* @return bool True if the passwords match.
* @task hashing
*/
public static function comparePassword(
PhutilOpaqueEnvelope $password,
PhutilOpaqueEnvelope $hash) {
$hasher = self::getHasherForHash($hash);
$parts = self::parseHashFromStorage($hash);
return $hasher->verifyPassword($password, $parts['hash']);
}
/**
* Get the human-readable algorithm name for a given hash.
*
* @param PhutilOpaqueEnvelope Storage hash.
* @return string Human-readable algorithm name.
*/
public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) {
$raw_hash = $hash->openEnvelope();
if (!strlen($raw_hash)) {
return pht('None');
}
try {
$current_hasher = self::getHasherForHash($hash);
return $current_hasher->getHumanReadableName();
} catch (Exception $ex) {
$info = self::parseHashFromStorage($hash);
$name = $info['name'];
return pht('Unknown ("%s")', $name);
}
}
/**
* Get the human-readable algorithm name for the best available hash.
*
* @return string Human-readable name for best hash.
*/
public static function getBestAlgorithmName() {
try {
$best_hasher = self::getBestHasher();
return $best_hasher->getHumanReadableName();
} catch (Exception $ex) {
return pht('Unknown');
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jul 29, 9:09 AM (3 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
188424
Default Alt Text
(230 KB)

Event Timeline