diff --git a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php index cb1dea3533..442cb08f66 100644 --- a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php @@ -1,95 +1,107 @@ <?php final class PhabricatorPolicyRequestExceptionHandler extends PhabricatorRequestExceptionHandler { public function getRequestExceptionHandlerPriority() { return 320000; } public function getRequestExceptionHandlerDescription() { return pht( 'Handles policy exceptions which occur when a user tries to '. 'do something they do not have permission to do.'); } public function canHandleRequestThrowable( AphrontRequest $request, $throwable) { if (!$this->isPhabricatorSite($request)) { return false; } return ($throwable instanceof PhabricatorPolicyException); } public function handleRequestThrowable( AphrontRequest $request, $throwable) { $viewer = $this->getViewer($request); if (!$viewer->isLoggedIn()) { // If the user isn't logged in, just give them a login form. This is // probably a generally more useful response than a policy dialog that // they have to click through to get a login form. // // Possibly we should add a header here like "you need to login to see // the thing you are trying to look at". $auth_app_class = 'PhabricatorAuthApplication'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); return id(new PhabricatorAuthStartController()) ->setRequest($request) ->setCurrentApplication($auth_app) ->handleRequest($request); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $throwable->getRejection()), ); $list = null; if ($throwable->getCapabilityName()) { $list = $throwable->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = $item; } $content[] = phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht( 'Users with the "%s" capability:', $throwable->getCapabilityName())); } $dialog = id(new AphrontDialogView()) ->setTitle($throwable->getTitle()) ->setClass('aphront-access-dialog') ->setUser($viewer) ->appendChild($content); if ($list) { $dialog->appendList($list); } + // If the install is in developer mode, include a stack trace for the + // exception. When debugging things, it isn't always obvious where a + // policy exception came from and this can make it easier to hunt down + // bugs or improve ambiguous/confusing messaging. + + $is_developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + if ($is_developer) { + $dialog->appendChild( + id(new AphrontStackTraceView()) + ->setTrace($throwable->getTrace())); + } + if ($request->isAjax()) { $dialog->addCancelButton('/', pht('Close')); } else { $dialog->addCancelButton('/', pht('OK')); } return $dialog; } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index e276e035e4..1cabeb0709 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,666 +1,666 @@ <?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 PhabricatorLiskDAO implements PhabricatorPolicyInterface, PhabricatorApplicationTransactionInterface { 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'), ); } final public function getApplicationName() { return 'application'; } final public function getTableName() { return 'application_application'; } final protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } final public function generatePHID() { return $this->getPHID(); } final public function save() { // When "save()" is called on applications, we just return without // actually writing anything to the database. return $this; } /* -( 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 * unlaunchable 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 getIcon() { 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) { foreach ($articles as $article) { $item = id(new PhabricatorActionView()) ->setName($article['name']) ->setHref($article['href']) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } $command_specs = $this->getMailCommandObjects(); if ($command_specs) { foreach ($command_specs as $key => $spec) { $object = $spec['object']; $class = get_class($this); $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; $item = id(new PhabricatorActionView()) ->setName($spec['name']) ->setHref($href) ->addSigil('help-item') ->setOpenInNewWindow(true); $items[] = $item; } } if ($items) { $divider = id(new PhabricatorActionView()) ->addSigil('help-item') ->setType(PhabricatorActionView::TYPE_DIVIDER); array_unshift($items, $divider); } return array_values($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(); } public function getResourceRoutes() { 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 )----------------------------------------------------- */ /** * 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(); } /* -( 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 PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setSortMethod('getApplicationOrder') ->execute(); // Reorder the applications into "application order". Notably, this // ensures their event handlers register in application order. $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_fragment = $viewer->getCacheFragment(); $key = 'app.'.$class.'.installed.'.$viewer_fragment; $result = $cache->getKey($key); if ($result === null) { if (!self::isClassInstalled($class)) { $result = false; } else { $application = self::getByClass($class); if (!$application->canUninstall()) { // If the application can not be uninstalled, always allow viewers // to see it. In particular, this allows logged-out viewers to see // Settings and load global default settings even if the install // does not allow public viewers. $result = true; } 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; } /* -( 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; + return true; 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(); } protected function getEditRoutePattern($base = null) { return $base.'(?:'. '(?P<id>[0-9]\d*)/)?'. '(?:'. '(?:'. '(?P<editAction>parameters|nodefault|nocreate|nomanage|comment)/'. '|'. '(?:form/(?P<formKey>[^/]+)/)?(?:page/(?P<pageKey>[^/]+)/)?'. ')'. ')?'; } protected function getBulkRoutePattern($base = null) { return $base.'(?:query/(?P<queryKey>[^/]+)/)?'; } protected function getQueryRoutePattern($base = null) { return $base.'(?:query/(?P<queryKey>[^/]+)/(?:(?P<queryAction>[^/]+)/)?)?'; } protected function getProfileMenuRouting($controller) { $edit_route = $this->getEditRoutePattern(); $mode_route = '(?P<itemEditMode>global|custom)/'; return array( '(?P<itemAction>view)/(?P<itemID>[^/]+)/' => $controller, '(?P<itemAction>hide)/(?P<itemID>[^/]+)/' => $controller, '(?P<itemAction>default)/(?P<itemID>[^/]+)/' => $controller, '(?P<itemAction>configure)/' => $controller, '(?P<itemAction>configure)/'.$mode_route => $controller, '(?P<itemAction>reorder)/'.$mode_route => $controller, '(?P<itemAction>edit)/'.$edit_route => $controller, '(?P<itemAction>new)/'.$mode_route.'(?<itemKey>[^/]+)/'.$edit_route => $controller, '(?P<itemAction>builtin)/(?<itemID>[^/]+)/'.$edit_route => $controller, ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorApplicationEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorApplicationApplicationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/config/editor/PhabricatorConfigEditor.php b/src/applications/config/editor/PhabricatorConfigEditor.php index f776c3ec0c..deccf1ef5c 100644 --- a/src/applications/config/editor/PhabricatorConfigEditor.php +++ b/src/applications/config/editor/PhabricatorConfigEditor.php @@ -1,160 +1,165 @@ <?php final class PhabricatorConfigEditor extends PhabricatorApplicationTransactionEditor { public function getEditorApplicationClass() { return 'PhabricatorConfigApplication'; } public function getEditorObjectsDescription() { return pht('Phabricator Configuration'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorConfigTransaction::TYPE_EDIT; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return array( 'deleted' => (int)$object->getIsDeleted(), 'value' => $object->getValue(), ); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: $v = $xaction->getNewValue(); // If this is a defined configuration option (vs a straggler from an // old version of Phabricator or a configuration file misspelling) // submit it to the validation gauntlet. $key = $object->getConfigKey(); $all_options = PhabricatorApplicationConfigOptions::loadAllOptions(); $option = idx($all_options, $key); if ($option) { $option->getGroup()->validateOption( $option, $v['value']); } $object->setIsDeleted((int)$v['deleted']); $object->setValue($v['value']); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: return $v; } return parent::mergeTransactions($u, $v); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: // If an edit deletes an already-deleted entry, no-op it. if (idx($old, 'deleted') && idx($new, 'deleted')) { return false; } break; } return parent::transactionHasEffect($object, $xaction); } protected function didApplyTransactions($object, array $xactions) { // Force all the setup checks to run on the next page load. PhabricatorSetupCheck::deleteSetupCheckCache(); return $xactions; } public static function storeNewValue( PhabricatorUser $user, PhabricatorConfigEntry $config_entry, $value, - PhabricatorContentSource $source) { + PhabricatorContentSource $source, + $acting_as_phid = null) { $xaction = id(new PhabricatorConfigTransaction()) ->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT) ->setNewValue( array( 'deleted' => false, 'value' => $value, )); $editor = id(new PhabricatorConfigEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSource($source); + if ($acting_as_phid) { + $editor->setActingAsPHID($acting_as_phid); + } + $editor->applyTransactions($config_entry, array($xaction)); } public static function deleteConfig( PhabricatorUser $user, PhabricatorConfigEntry $config_entry, PhabricatorContentSource $source) { $xaction = id(new PhabricatorConfigTransaction()) ->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT) ->setNewValue( array( 'deleted' => true, 'value' => null, )); $editor = id(new PhabricatorConfigEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSource($source); $editor->applyTransactions($config_entry, array($xaction)); } } diff --git a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php index 91b5c73249..59dfc09486 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php @@ -1,197 +1,206 @@ <?php final class PhabricatorApplicationPolicyChangeTransaction extends PhabricatorApplicationTransactionType { const TRANSACTIONTYPE = 'application.policy'; const METADATA_ATTRIBUTE = 'capability.name'; private $policies; public function generateOldValue($object) { $application = $object; $capability = $this->getCapabilityName(); return $application->getPolicy($capability); } public function applyExternalEffects($object, $value) { $application = $object; $user = $this->getActor(); $key = 'phabricator.application-settings'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $current_value = $config_entry->getValue(); $phid = $application->getPHID(); if (empty($current_value[$phid])) { $current_value[$application->getPHID()] = array(); } if (empty($current_value[$phid]['policy'])) { $current_value[$phid]['policy'] = array(); } $new = array($this->getCapabilityName() => $value); $current_value[$phid]['policy'] = $new + $current_value[$phid]['policy']; $editor = $this->getEditor(); $content_source = $editor->getContentSource(); + + // NOTE: We allow applications to have custom edit policies, but they are + // currently stored in the Config application. The ability to edit Config + // values is always restricted to administrators, today. Empower this + // particular edit to punch through possible stricter policies, so normal + // users can change application configuration if the application allows + // them to do so. + PhabricatorConfigEditor::storeNewValue( - $user, + PhabricatorUser::getOmnipotentUser(), $config_entry, $current_value, - $content_source); + $content_source, + $user->getPHID()); } public function getTitle() { $old = $this->renderPolicy($this->getOldValue()); $new = $this->renderPolicy($this->getNewValue()); return pht( '%s changed the "%s" policy from "%s" to "%s".', $this->renderAuthor(), $this->renderCapability(), $old, $new); } public function getTitleForFeed() { $old = $this->renderPolicy($this->getOldValue()); $new = $this->renderPolicy($this->getNewValue()); return pht( '%s changed the "%s" policy for application %s from "%s" to "%s".', $this->renderAuthor(), $this->renderCapability(), $this->renderObject(), $old, $new); } public function validateTransactions($object, array $xactions) { $user = $this->getActor(); $application = $object; $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($application) ->execute(); $errors = array(); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); $capability = $xaction->getMetadataValue(self::METADATA_ATTRIBUTE); if (empty($policies[$new])) { // Not a standard policy, check for a custom policy. $policy = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->withPHIDs(array($new)) ->executeOne(); if (!$policy) { $errors[] = $this->newInvalidError( pht('Policy does not exist.')); continue; } } else { $policy = idx($policies, $new); } if (!$policy->isValidPolicyForEdit()) { $errors[] = $this->newInvalidError( pht('Can\'t set the policy to a policy you can\'t view!')); continue; } if ($new == PhabricatorPolicies::POLICY_PUBLIC) { $capobj = PhabricatorPolicyCapability::getCapabilityByKey( $capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { $errors[] = $this->newInvalidError( pht('Can\'t set non-public policies to public.')); continue; } } if (!$application->isCapabilityEditable($capability)) { $errors[] = $this->newInvalidError( pht('Capability "%s" is not editable for this application.', $capability)); continue; } } // If we're changing these policies, the viewer needs to still be able to // view or edit the application under the new policy. $validate_map = array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); $validate_map = array_fill_keys($validate_map, array()); foreach ($xactions as $xaction) { $capability = $xaction->getMetadataValue(self::METADATA_ATTRIBUTE); if (!isset($validate_map[$capability])) { continue; } $validate_map[$capability][] = $xaction; } foreach ($validate_map as $capability => $cap_xactions) { if (!$cap_xactions) { continue; } $editor = $this->getEditor(); $policy_errors = $editor->validatePolicyTransaction( $object, $cap_xactions, self::TRANSACTIONTYPE, $capability); foreach ($policy_errors as $error) { $errors[] = $error; } } return $errors; } private function renderPolicy($name) { $policies = $this->getAllPolicies(); if (empty($policies[$name])) { // Not a standard policy, check for a custom policy. $policy = id(new PhabricatorPolicyQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($name)) ->executeOne(); $policies[$name] = $policy; } $policy = idx($policies, $name); return $this->renderValue($policy->getFullName()); } private function getAllPolicies() { if (!$this->policies) { $viewer = $this->getViewer(); $application = $this->getObject(); $this->policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($application) ->execute(); } return $this->policies; } private function renderCapability() { $application = $this->getObject(); $capability = $this->getCapabilityName(); return $application->getCapabilityLabel($capability); } private function getCapabilityName() { return $this->getMetadataValue(self::METADATA_ATTRIBUTE); } } diff --git a/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php index 12fbc8ebd4..b76e63b5c0 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php @@ -1,79 +1,83 @@ <?php final class PhabricatorApplicationUninstallTransaction extends PhabricatorApplicationTransactionType { const TRANSACTIONTYPE = 'application.uninstall'; public function generateOldValue($object) { $key = 'phabricator.uninstalled-applications'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[get_class($object)])) { return 'uninstalled'; } else { return 'installed'; } } public function generateNewValue($object, $value) { if ($value === 'uninstall') { return 'uninstalled'; } else { return 'installed'; } } public function applyExternalEffects($object, $value) { $application = $object; $user = $this->getActor(); $key = 'phabricator.uninstalled-applications'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $list = $config_entry->getValue(); $uninstalled = PhabricatorEnv::getEnvConfig($key); if (isset($uninstalled[get_class($application)])) { unset($list[get_class($application)]); } else { $list[get_class($application)] = true; } $editor = $this->getEditor(); $content_source = $editor->getContentSource(); + + // Today, changing config requires "Administrator", but "Can Edit" on + // applications to let you uninstall them may be granted to any user. PhabricatorConfigEditor::storeNewValue( - $user, + PhabricatorUser::getOmnipotentUser(), $config_entry, $list, - $content_source); + $content_source, + $user->getPHID()); } public function getTitle() { if ($this->getNewValue() === 'uninstalled') { return pht( '%s uninstalled this application.', $this->renderAuthor()); } else { return pht( '%s installed this application.', $this->renderAuthor()); } } public function getTitleForFeed() { if ($this->getNewValue() === 'uninstalled') { return pht( '%s uninstalled %s.', $this->renderAuthor(), $this->renderObject()); } else { return pht( '%s installed %s.', $this->renderAuthor(), $this->renderObject()); } } }