diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index 3f694207b9..2c21f63407 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -1,145 +1,145 @@ getViewer(); $this->providerKey = $request->getURIData('pkey'); list($type, $domain) = explode(':', $this->providerKey, 2); // Check that this account link actually exists. We don't require the // provider to exist because we want users to be able to delete links to // dead accounts if they want. $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND userPHID = %s', $type, $domain, $viewer->getPHID()); if (!$account) { return $this->renderNoAccountErrorDialog(); } // Check that the provider (if it exists) allows accounts to be unlinked. $provider_key = $this->providerKey; $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key); if ($provider) { if (!$provider->shouldAllowAccountUnlink()) { return $this->renderNotUnlinkableErrorDialog($provider); } } // Check that this account isn't the last account which can be used to // login. We prevent you from removing the last account. if ($account->isUsableForLogin()) { $other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $viewer->getPHID()); $valid_accounts = 0; foreach ($other_accounts as $other_account) { if ($other_account->isUsableForLogin()) { $valid_accounts++; } } if ($valid_accounts < 2) { return $this->renderLastUsableAccountErrorDialog(); } } if ($request->isDialogFormPost()) { $account->delete(); id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $viewer, $request->getCookie(PhabricatorCookies::COOKIE_SESSION)); return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); } - return $this->renderConfirmDialog($account); + return $this->renderConfirmDialog(); } private function getDoneURI() { return '/settings/panel/external/'; } private function renderNoAccountErrorDialog() { $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle(pht('No Such Account')) ->appendChild( pht( 'You can not unlink this account because it is not linked.')) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } private function renderNotUnlinkableErrorDialog( PhabricatorAuthProvider $provider) { $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle(pht('Permanent Account Link')) ->appendChild( pht( 'You can not unlink this account because the administrator has '. 'configured Phabricator to make links to %s accounts permanent.', $provider->getProviderName())) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } private function renderLastUsableAccountErrorDialog() { $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle(pht('Last Valid Account')) ->appendChild( pht( 'You can not unlink this account because you have no other '. 'valid login accounts. If you removed it, you would be unable '. 'to login. Add another authentication method before removing '. 'this one.')) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } private function renderConfirmDialog() { $provider_key = $this->providerKey; $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key); if ($provider) { $title = pht('Unlink "%s" Account?', $provider->getProviderName()); $body = pht( 'You will no longer be able to use your %s account to '. 'log in to Phabricator.', $provider->getProviderName()); } else { $title = pht('Unlink Account?'); $body = pht( 'You will no longer be able to use this account to log in '. 'to Phabricator.'); } $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle($title) ->appendParagraph($body) ->appendParagraph( pht( 'Note: Unlinking an authentication provider will terminate any '. 'other active login sessions.')) ->addSubmitButton(pht('Unlink Account')) ->addCancelButton($this->getDoneURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/badges/controller/PhabricatorBadgesAwardController.php b/src/applications/badges/controller/PhabricatorBadgesAwardController.php index 0e5679caa4..f9294ec5c6 100644 --- a/src/applications/badges/controller/PhabricatorBadgesAwardController.php +++ b/src/applications/badges/controller/PhabricatorBadgesAwardController.php @@ -1,77 +1,77 @@ getViewer(); $id = $request->getURIData('id'); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$user) { return new Aphront404Response(); } $view_uri = '/p/'.$user->getUsername(); if ($request->isFormPost()) { $badge_phids = $request->getArr('badgePHIDs'); $badges = id(new PhabricatorBadgesQuery()) ->setViewer($viewer) ->withPHIDs($badge_phids) ->needRecipients(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_VIEW, )) ->execute(); if (!$badges) { return new Aphront404Response(); } $award_phids = array($user->getPHID()); foreach ($badges as $badge) { $xactions = array(); $xactions[] = id(new PhabricatorBadgesTransaction()) ->setTransactionType(PhabricatorBadgesTransaction::TYPE_AWARD) ->setNewValue($award_phids); - $editor = id(new PhabricatorBadgesEditor($badge)) + $editor = id(new PhabricatorBadgesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($badge, $xactions); } return id(new AphrontRedirectResponse()) ->setURI($view_uri); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Badge')) ->setName('badgePHIDs') ->setDatasource( id(new PhabricatorBadgesDatasource()) ->setParameters( array( 'recipientPHID' => $user->getPHID(), )))); $dialog = $this->newDialog() ->setTitle(pht('Grant Badge')) ->appendForm($form) ->addCancelButton($view_uri) ->addSubmitButton(pht('Award')); return $dialog; } } diff --git a/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php b/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php index 2f794e4470..546c41a1d4 100644 --- a/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php +++ b/src/applications/badges/controller/PhabricatorBadgesEditRecipientsController.php @@ -1,96 +1,96 @@ getViewer(); $id = $request->getURIData('id'); $xactions = array(); $badge = id(new PhabricatorBadgesQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needRecipients(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_VIEW, )) ->executeOne(); if (!$badge) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('recipients/'.$badge->getID().'/'); $awards = $badge->getAwards(); $recipient_phids = mpull($awards, 'getRecipientPHID'); if ($request->isFormPost()) { $award_phids = array(); $add_recipients = $request->getArr('phids'); if ($add_recipients) { foreach ($add_recipients as $phid) { $award_phids[] = $phid; } } $xactions[] = id(new PhabricatorBadgesTransaction()) ->setTransactionType(PhabricatorBadgesTransaction::TYPE_AWARD) ->setNewValue($award_phids); - $editor = id(new PhabricatorBadgesEditor($badge)) + $editor = id(new PhabricatorBadgesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($badge, $xactions); return id(new AphrontRedirectResponse()) ->setURI($view_uri); } $recipient_phids = array_reverse($recipient_phids); $handles = $this->loadViewerHandles($recipient_phids); $state = array(); foreach ($handles as $handle) { $state[] = array( 'phid' => $handle->getPHID(), 'name' => $handle->getFullName(), ); } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $badge, PhabricatorPolicyCapability::CAN_EDIT); $form_box = null; $title = pht('Add Recipient'); if ($can_edit) { $header_name = pht('Edit Recipients'); $form = new AphrontFormView(); $form ->setUser($viewer) ->setFullWidth(true) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel(pht('Recipients')) ->setDatasource(new PhabricatorPeopleDatasource())); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Add Recipients')) ->appendForm($form) ->addCancelButton($view_uri) ->addSubmitButton(pht('Add Recipients')); return $dialog; } } diff --git a/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php b/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php index c77a5f33ea..0185698686 100644 --- a/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php +++ b/src/applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php @@ -1,70 +1,70 @@ getViewer(); $id = $request->getURIData('id'); $badge = id(new PhabricatorBadgesQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needRecipients(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$badge) { return new Aphront404Response(); } $awards = $badge->getAwards(); $recipient_phids = mpull($awards, 'getRecipientPHID'); $remove_phid = $request->getStr('phid'); if (!in_array($remove_phid, $recipient_phids)) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('view/'.$badge->getID().'/'); if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PhabricatorBadgesTransaction()) ->setTransactionType(PhabricatorBadgesTransaction::TYPE_REVOKE) ->setNewValue(array($remove_phid)); - $editor = id(new PhabricatorBadgesEditor($badge)) + $editor = id(new PhabricatorBadgesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($badge, $xactions); return id(new AphrontRedirectResponse()) ->setURI($view_uri); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($remove_phid)) ->executeOne(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Really Revoke Badge?')) ->appendParagraph( pht( 'Really revoke the badge "%s" from %s?', phutil_tag('strong', array(), $badge->getName()), phutil_tag('strong', array(), $handle->getName()))) ->addCancelButton($view_uri) ->addSubmitButton(pht('Revoke Badge')); return $dialog; } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index b791aaeef8..93b738d9e0 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,616 +1,616 @@ 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 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'); + 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 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; 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[0-9]\d*)/)?'. '(?:'. '(?:'. '(?Pparameters|nodefault|nocreate|nomanage|comment)/'. '|'. '(?:form/(?P[^/]+)/)?(?:page/(?P[^/]+)/)?'. ')'. ')?'; } protected function getQueryRoutePattern($base = null) { return $base.'(?:query/(?P[^/]+)/)?'; } protected function getProfileMenuRouting($controller) { $edit_route = $this->getEditRoutePattern(); $mode_route = '(?Pglobal|custom)/'; return array( '(?Pview)/(?P[^/]+)/' => $controller, '(?Phide)/(?P[^/]+)/' => $controller, '(?Pdefault)/(?P[^/]+)/' => $controller, '(?Pconfigure)/' => $controller, '(?Pconfigure)/'.$mode_route => $controller, '(?Preorder)/'.$mode_route => $controller, '(?Pedit)/'.$edit_route => $controller, '(?Pnew)/'.$mode_route.'(?[^/]+)/'.$edit_route => $controller, '(?Pbuiltin)/(?[^/]+)/'.$edit_route => $controller, ); } } diff --git a/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php b/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php index aa44acec70..646badfe9c 100644 --- a/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php +++ b/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php @@ -1,62 +1,60 @@ overrideTimezoneIdentifier('America/Los_Angeles'); $days = $this->getAllDays(); foreach ($days as $day) { - $data = CalendarTimeUtil::getCalendarWidgetTimestamps( - $u, - $day); + $data = CalendarTimeUtil::getTimestamps($u, $day, 1); $this->assertEqual( '000000', $data['epoch_stamps'][0]->format('His')); } } public function testTimestampsStartDay() { $u = new PhabricatorUser(); $u->overrideTimezoneIdentifier('America/Los_Angeles'); $days = $this->getAllDays(); foreach ($days as $day) { $data = CalendarTimeUtil::getTimestamps( $u, $day, 1); $this->assertEqual( $day, $data['epoch_stamps'][0]->format('l')); } $t = 1370202281; // 2013-06-02 12:44:41 -0700 -- a Sunday $time = PhabricatorTime::pushTime($t, 'America/Los_Angeles'); foreach ($days as $day) { $data = CalendarTimeUtil::getTimestamps( $u, $day, 1); $this->assertEqual( $day, $data['epoch_stamps'][0]->format('l')); } unset($time); } private function getAllDays() { return array( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ); } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php index 27a23d1e17..80679a9533 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php +++ b/src/applications/calendar/storage/PhabricatorCalendarExternalInvitee.php @@ -1,71 +1,71 @@ setInviterPHID($actor->getPHID()) - ->setStatus(self::STATUS_INVITED) + ->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED) ->setEventPHID($event->getPHID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'nameIndex' => 'bytes12', 'uri' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('nameIndex'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorCalendarExternalInviteePHIDType::TYPECONST; } public function save() { $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); return parent::save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index 6187421e16..8588bc07f9 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -1,296 +1,296 @@ getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorConduitSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->addLabel('Logs'); $nav->addFilter('log', pht('Call Logs')); $nav->selectFilter(null); return $nav; } public function buildApplicationMenu() { return $this->buildSideNavView()->getMenu(); } protected function renderExampleBox(ConduitAPIMethod $method, $params) { $viewer = $this->getViewer(); $arc_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'arc', $params)); $curl_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'curl', $params)); $php_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'php', $params)); $panel_uri = id(new PhabricatorConduitTokensSettingsPanel()) ->setViewer($viewer) ->setUser($viewer) ->getPanelURI(); $panel_link = phutil_tag( 'a', array( 'href' => $panel_uri, ), pht('Conduit API Tokens')); $panel_link = phutil_tag('strong', array(), $panel_link); $messages = array( pht( 'Use the %s panel in Settings to generate or manage API tokens.', $panel_link), ); $info_view = id(new PHUIInfoView()) ->setErrors($messages) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $tab_group = id(new PHUITabGroupView()) ->addTab( id(new PHUITabView()) ->setName(pht('arc call-conduit')) ->setKey('arc') ->appendChild($arc_example)) ->addTab( id(new PHUITabView()) ->setName(pht('cURL')) ->setKey('curl') ->appendChild($curl_example)) ->addTab( id(new PHUITabView()) ->setName(pht('PHP')) ->setKey('php') ->appendChild($php_example)); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Examples')) ->setInfoView($info_view) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); } private function renderExample( ConduitAPIMethod $method, $kind, $params) { switch ($kind) { case 'arc': $example = $this->buildArcanistExample($method, $params); break; case 'php': $example = $this->buildPHPExample($method, $params); break; case 'curl': $example = $this->buildCURLExample($method, $params); break; default: throw new Exception(pht('Conduit client "%s" is not known.', $kind)); } return $example; } private function buildArcanistExample( ConduitAPIMethod $method, $params) { $parts = array(); $parts[] = '$ echo '; if ($params === null) { $parts[] = phutil_tag('strong', array(), ''); } else { $params = $this->simplifyParams($params); $params = id(new PhutilJSON())->encodeFormatted($params); $params = trim($params); $params = csprintf('%s', $params); $parts[] = phutil_tag('strong', array('class' => 'real'), $params); } $parts[] = ' | '; $parts[] = 'arc call-conduit '; $parts[] = '--conduit-uri '; $parts[] = phutil_tag( 'strong', array('class' => 'real'), PhabricatorEnv::getURI('/')); $parts[] = ' '; $parts[] = '--conduit-token '; $parts[] = phutil_tag('strong', array(), ''); $parts[] = ' '; $parts[] = $method->getAPIMethodName(); return $this->renderExampleCode($parts); } private function buildPHPExample( ConduitAPIMethod $method, $params) { $parts = array(); $libphutil_path = 'path/to/libphutil/src/__phutil_library_init__.php'; $parts[] = '')); $parts[] = "\";\n"; $parts[] = '$api_parameters = '; if ($params === null) { $parts[] = 'array('; $parts[] = phutil_tag('strong', array(), pht('')); $parts[] = ');'; } else { $params = $this->simplifyParams($params); - $params = phutil_var_export($params, true); + $params = phutil_var_export($params); $parts[] = phutil_tag('strong', array('class' => 'real'), $params); $parts[] = ';'; } $parts[] = "\n\n"; $parts[] = '$client = new ConduitClient('; $parts[] = phutil_tag( 'strong', array('class' => 'real'), - phutil_var_export(PhabricatorEnv::getURI('/'), true)); + phutil_var_export(PhabricatorEnv::getURI('/'))); $parts[] = ");\n"; $parts[] = '$client->setConduitToken($api_token);'; $parts[] = "\n\n"; $parts[] = '$result = $client->callMethodSynchronous('; $parts[] = phutil_tag( 'strong', array('class' => 'real'), - phutil_var_export($method->getAPIMethodName(), true)); + phutil_var_export($method->getAPIMethodName())); $parts[] = ', '; $parts[] = '$api_parameters'; $parts[] = ");\n"; $parts[] = 'print_r($result);'; return $this->renderExampleCode($parts); } private function buildCURLExample( ConduitAPIMethod $method, $params) { $call_uri = '/api/'.$method->getAPIMethodName(); $parts = array(); $linebreak = array('\\', phutil_tag('br'), ' '); $parts[] = '$ curl '; $parts[] = phutil_tag( 'strong', array('class' => 'real'), csprintf('%R', PhabricatorEnv::getURI($call_uri))); $parts[] = ' '; $parts[] = $linebreak; $parts[] = '-d api.token='; $parts[] = phutil_tag('strong', array(), 'api-token'); $parts[] = ' '; $parts[] = $linebreak; if ($params === null) { $parts[] = '-d '; $parts[] = phutil_tag('strong', array(), 'param'); $parts[] = '='; $parts[] = phutil_tag('strong', array(), 'value'); $parts[] = ' '; $parts[] = $linebreak; $parts[] = phutil_tag('strong', array(), '...'); } else { $lines = array(); $params = $this->simplifyParams($params); foreach ($params as $key => $value) { $pieces = $this->getQueryStringParts(null, $key, $value); foreach ($pieces as $piece) { $lines[] = array( '-d ', phutil_tag('strong', array('class' => 'real'), $piece), ); } } $parts[] = phutil_implode_html(array(' ', $linebreak), $lines); } return $this->renderExampleCode($parts); } private function renderExampleCode($example) { require_celerity_resource('conduit-api-css'); return phutil_tag( 'div', array( 'class' => 'PhabricatorMonospaced conduit-api-example-code', ), $example); } private function simplifyParams(array $params) { foreach ($params as $key => $value) { if ($value === null) { unset($params[$key]); } } return $params; } private function getQueryStringParts($prefix, $key, $value) { if ($prefix === null) { $head = phutil_escape_uri($key); } else { $head = $prefix.'['.phutil_escape_uri($key).']'; } if (!is_array($value)) { return array( $head.'='.phutil_escape_uri($value), ); } $results = array(); foreach ($value as $subkey => $subvalue) { $subparts = $this->getQueryStringParts($head, $subkey, $subvalue); foreach ($subparts as $subpart) { $results[] = $subpart; } } return $results; } } diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php index 1802e608e9..46ee4efe77 100644 --- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php +++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php @@ -1,87 +1,87 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_RUNNING) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if (!$task_daemon) { - $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd'); + $doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd'); $summary = pht( 'You must start the Phabricator daemons to send email, rebuild '. 'search indexes, and do other background processing.'); $message = pht( 'The Phabricator daemons are not running, so Phabricator will not '. 'be able to perform background processing (including sending email, '. 'rebuilding search indexes, importing commits, cleaning up old data, '. 'and running builds).'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Phabricator Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd start'); } $expect_user = PhabricatorEnv::getEnvConfig('phd.user'); if (strlen($expect_user)) { $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); foreach ($all_daemons as $daemon) { $actual_user = $daemon->getRunningAsUser(); if ($actual_user == $expect_user) { continue; } $summary = pht( 'At least one daemon is currently running as the wrong user.'); $message = pht( 'A daemon is running as user %s, but daemons should be '. 'running as %s.'. "\n\n". 'Either adjust the configuration setting %s or restart the '. 'daemons. Daemons should attempt to run as the proper user when '. 'restarted.', phutil_tag('tt', array(), $actual_user), phutil_tag('tt', array(), $expect_user), phutil_tag('tt', array(), 'phd.user')); $this->newIssue('daemons.run-as-different-user') ->setName(pht('Daemon Running as Wrong User')) ->setSummary($summary) ->setMessage($message) ->addPhabricatorConfig('phd.user') ->addCommand('phabricator/ $ ./bin/phd restart'); break; } } } } diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php index 1dd3add94d..5382a27b7a 100644 --- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php @@ -1,72 +1,72 @@ $doc_href, 'target' => '_blank', ), pht('Configuring a Preamble Script'))); $this->newIssue('php.remote_addr') ->setName(pht('No REMOTE_ADDR available')) ->setSummary($summary) ->setMessage($message); } if (version_compare(phpversion(), '7', '>=')) { // This option was removed in PHP7. $raw_post_data = -1; } else { $raw_post_data = (int)ini_get('always_populate_raw_post_data'); } if ($raw_post_data != -1) { $summary = pht( 'PHP setting "%s" should be set to "-1" to avoid deprecation '. 'warnings.', 'always_populate_raw_post_data'); $message = pht( 'The "%s" key is set to some value other than "-1" in your PHP '. 'configuration. This can cause PHP to raise deprecation warnings '. 'during process startup. Set this option to "-1" to prevent these '. 'warnings from appearing.', 'always_populate_raw_post_data'); $this->newIssue('php.always_populate_raw_post_data') ->setName(pht('Disable PHP %s', 'always_populate_raw_post_data')) ->setSummary($summary) ->setMessage($message) ->addPHPConfig('always_populate_raw_post_data'); } } } diff --git a/src/applications/config/check/PhabricatorSecuritySetupCheck.php b/src/applications/config/check/PhabricatorSecuritySetupCheck.php index d7ae7db53a..518f6c7d54 100644 --- a/src/applications/config/check/PhabricatorSecuritySetupCheck.php +++ b/src/applications/config/check/PhabricatorSecuritySetupCheck.php @@ -1,78 +1,78 @@ '() { :;} ; echo VULNERABLE', ); list($err, $stdout) = id(new ExecFuture('echo shellshock-test')) ->setEnv($payload, $wipe_process_env = true) ->resolve(); if (!$err && preg_match('/VULNERABLE/', $stdout)) { $summary = pht( 'This system has an unpatched version of Bash with a severe, widely '. 'disclosed vulnerability.'); $message = pht( 'The version of %s on this system is out of date and contains a '. 'major, widely disclosed vulnerability (the "Shellshock" '. 'vulnerability).'. "\n\n". 'Upgrade %s to a patched version.'. "\n\n". 'To learn more about how this issue affects Phabricator, see %s.', phutil_tag('tt', array(), 'bash'), phutil_tag('tt', array(), 'bash'), phutil_tag( 'a', array( 'href' => 'https://secure.phabricator.com/T6185', 'target' => '_blank', ), pht('T6185 "Shellshock" Bash Vulnerability'))); $this ->newIssue('security.shellshock') ->setName(pht('Severe Security Vulnerability: Unpatched Bash')) ->setSummary($summary) ->setMessage($message); } $file_key = 'security.alternate-file-domain'; $file_domain = PhabricatorEnv::getEnvConfig($file_key); if (!$file_domain) { - $doc_href = PhabricatorEnv::getDocLink('Configuring a File Domain'); + $doc_href = PhabricatorEnv::getDoclink('Configuring a File Domain'); $this->newIssue('security.'.$file_key) ->setName(pht('Alternate File Domain Not Configured')) ->setSummary( pht( 'Increase security (and improve performance) by configuring '. 'a CDN or alternate file domain.')) ->setMessage( pht( 'Phabricator is currently configured to serve user uploads '. 'directly from the same domain as other content. This is a '. 'security risk.'. "\n\n". 'Configure a CDN (or alternate file domain) to eliminate this '. 'risk. Using a CDN will also improve performance. See the '. 'guide below for instructions.')) ->addPhabricatorConfig($file_key) ->addLink( $doc_href, pht('Configuration Guide: Configuring a File Domain')); } } } diff --git a/src/applications/config/check/PhabricatorStorageSetupCheck.php b/src/applications/config/check/PhabricatorStorageSetupCheck.php index 9ec753da05..09cecfe6d0 100644 --- a/src/applications/config/check/PhabricatorStorageSetupCheck.php +++ b/src/applications/config/check/PhabricatorStorageSetupCheck.php @@ -1,196 +1,196 @@ checkS3(); if (!$chunk_engine_active) { - $doc_href = PhabricatorEnv::getDocLink('Configuring File Storage'); + $doc_href = PhabricatorEnv::getDoclink('Configuring File Storage'); $message = pht( 'Large file storage has not been configured, which will limit '. 'the maximum size of file uploads. See %s for '. 'instructions on configuring uploads and storage.', phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Configuring File Storage'))); $this ->newIssue('large-files') ->setShortName(pht('Large Files')) ->setName(pht('Large File Storage Not Configured')) ->setMessage($message); } $post_max_size = ini_get('post_max_size'); if ($post_max_size && ((int)$post_max_size > 0)) { $post_max_bytes = phutil_parse_bytes($post_max_size); $post_max_need = (32 * 1024 * 1024); if ($post_max_need > $post_max_bytes) { $summary = pht( 'Set %s in your PHP configuration to at least 32MB '. 'to support large file uploads.', phutil_tag('tt', array(), 'post_max_size')); $message = pht( 'Adjust %s in your PHP configuration to at least 32MB. When '. 'set to smaller value, large file uploads may not work properly.', phutil_tag('tt', array(), 'post_max_size')); $this ->newIssue('php.post_max_size') ->setName(pht('PHP post_max_size Not Configured')) ->setSummary($summary) ->setMessage($message) ->setGroup(self::GROUP_PHP) ->addPHPConfig('post_max_size'); } } // This is somewhat arbitrary, but make sure we have enough headroom to // upload a default file at the chunk threshold (8MB), which may be // base64 encoded, then JSON encoded in the request, and may need to be // held in memory in the raw and as a query string. $need_bytes = (64 * 1024 * 1024); $memory_limit = PhabricatorStartup::getOldMemoryLimit(); if ($memory_limit && ((int)$memory_limit > 0)) { $memory_limit_bytes = phutil_parse_bytes($memory_limit); $memory_usage_bytes = memory_get_usage(); $available_bytes = ($memory_limit_bytes - $memory_usage_bytes); if ($need_bytes > $available_bytes) { $summary = pht( 'Your PHP memory limit is configured in a way that may prevent '. 'you from uploading large files or handling large requests.'); $message = pht( 'When you upload a file via drag-and-drop or the API, chunks must '. 'be buffered into memory before being written to permanent '. 'storage. Phabricator needs memory available to store these '. 'chunks while they are uploaded, but PHP is currently configured '. 'to severly limit the available memory.'. "\n\n". 'PHP processes currently have very little free memory available '. '(%s). To work well, processes should have at least %s.'. "\n\n". '(Note that the application itself must also fit in available '. 'memory, so not all of the memory under the memory limit is '. 'available for running workloads.)'. "\n\n". "The easiest way to resolve this issue is to set %s to %s in your ". "PHP configuration, to disable the memory limit. There is ". "usually little or no value to using this option to limit ". "Phabricator process memory.". "\n\n". "You can also increase the limit or ignore this issue and accept ". "that you may encounter problems uploading large files and ". "processing large requests.", phutil_format_bytes($available_bytes), phutil_format_bytes($need_bytes), phutil_tag('tt', array(), 'memory_limit'), phutil_tag('tt', array(), '-1')); $this ->newIssue('php.memory_limit.upload') ->setName(pht('Memory Limit Restricts File Uploads')) ->setSummary($summary) ->setMessage($message) ->setGroup(self::GROUP_PHP) ->addPHPConfig('memory_limit') ->addPHPConfigOriginalValue('memory_limit', $memory_limit); } } $local_path = PhabricatorEnv::getEnvConfig('storage.local-disk.path'); if (!$local_path) { return; } if (!Filesystem::pathExists($local_path) || !is_readable($local_path) || !is_writable($local_path)) { $message = pht( 'Configured location for storing uploaded files on disk ("%s") does '. 'not exist, or is not readable or writable. Verify the directory '. 'exists and is readable and writable by the webserver.', $local_path); $this ->newIssue('config.storage.local-disk.path') ->setShortName(pht('Local Disk Storage')) ->setName(pht('Local Disk Storage Not Readable/Writable')) ->setMessage($message) ->addPhabricatorConfig('storage.local-disk.path'); } } private function checkS3() { $access_key = PhabricatorEnv::getEnvConfig('amazon-s3.access-key'); $secret_key = PhabricatorEnv::getEnvConfig('amazon-s3.secret-key'); $region = PhabricatorEnv::getEnvConfig('amazon-s3.region'); $endpoint = PhabricatorEnv::getEnvConfig('amazon-s3.endpoint'); $how_many = 0; if (strlen($access_key)) { $how_many++; } if (strlen($secret_key)) { $how_many++; } if (strlen($region)) { $how_many++; } if (strlen($endpoint)) { $how_many++; } // Nothing configured, no issues here. if ($how_many === 0) { return; } // Everything configured, no issues here. if ($how_many === 4) { return; } $message = pht( 'File storage in Amazon S3 has been partially configured, but you are '. 'missing some required settings. S3 will not be available to store '. 'files until you complete the configuration. Either configure S3 fully '. 'or remove the partial configuration.'); $this->newIssue('storage.s3.partial-config') ->setShortName(pht('S3 Partially Configured')) ->setName(pht('Amazon S3 is Only Partially Configured')) ->setMessage($message) ->addPhabricatorConfig('amazon-s3.access-key') ->addPhabricatorConfig('amazon-s3.secret-key') ->addPhabricatorConfig('amazon-s3.region') ->addPhabricatorConfig('amazon-s3.endpoint'); } } diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index 8753531c7e..03dfa3f64c 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -1,245 +1,245 @@ buildSideNavView(); $nav->selectFilter('cluster/databases/'); $title = pht('Cluster Database Status'); $doc_href = PhabricatorEnv::getDoclink('Cluster: Databases'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true) ->addActionLink( id(new PHUIButtonView()) ->setIcon('fa-book') ->setHref($doc_href) ->setTag('a') ->setText(pht('Documentation'))); $crumbs = $this - ->buildApplicationCrumbs($nav) + ->buildApplicationCrumbs() ->addTextCrumb($title) ->setBorder(true); $database_status = $this->buildClusterDatabaseStatus(); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent($database_status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } private function buildClusterDatabaseStatus() { $viewer = $this->getViewer(); $databases = PhabricatorDatabaseRef::queryAll(); $connection_map = PhabricatorDatabaseRef::getConnectionStatusMap(); $replica_map = PhabricatorDatabaseRef::getReplicaStatusMap(); Javelin::initBehavior('phabricator-tooltips'); $rows = array(); foreach ($databases as $database) { $messages = array(); if ($database->getIsMaster()) { $role_icon = id(new PHUIIconView()) ->setIcon('fa-database sky') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Master'), )); } else { $role_icon = id(new PHUIIconView()) ->setIcon('fa-download') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Replica'), )); } if ($database->getDisabled()) { $conn_icon = 'fa-times'; $conn_color = 'grey'; $conn_label = pht('Disabled'); } else { $status = $database->getConnectionStatus(); $info = idx($connection_map, $status, array()); $conn_icon = idx($info, 'icon'); $conn_color = idx($info, 'color'); $conn_label = idx($info, 'label'); if ($status === PhabricatorDatabaseRef::STATUS_OKAY) { $latency = $database->getConnectionLatency(); $latency = (int)(1000000 * $latency); $conn_label = pht('%s us', new PhutilNumber($latency)); } } $connection = array( id(new PHUIIconView())->setIcon("{$conn_icon} {$conn_color}"), ' ', $conn_label, ); if ($database->getDisabled()) { $replica_icon = 'fa-times'; $replica_color = 'grey'; $replica_label = pht('Disabled'); } else { $status = $database->getReplicaStatus(); $info = idx($replica_map, $status, array()); $replica_icon = idx($info, 'icon'); $replica_color = idx($info, 'color'); $replica_label = idx($info, 'label'); if ($database->getIsMaster()) { if ($status === PhabricatorDatabaseRef::REPLICATION_OKAY) { $replica_icon = 'fa-database'; } } else { switch ($status) { case PhabricatorDatabaseRef::REPLICATION_OKAY: case PhabricatorDatabaseRef::REPLICATION_SLOW: $delay = $database->getReplicaDelay(); if ($delay) { $replica_label = pht('%ss Behind', new PhutilNumber($delay)); } else { $replica_label = pht('Up to Date'); } break; } } } $replication = array( id(new PHUIIconView())->setIcon("{$replica_icon} {$replica_color}"), ' ', $replica_label, ); $health = $database->getHealthRecord(); $health_up = $health->getUpEventCount(); $health_down = $health->getDownEventCount(); if ($health->getIsHealthy()) { $health_icon = id(new PHUIIconView()) ->setIcon('fa-plus green'); } else { $health_icon = id(new PHUIIconView()) ->setIcon('fa-times red'); $messages[] = pht( 'UNHEALTHY: This database has failed recent health checks. Traffic '. 'will not be sent to it until it recovers.'); } $health_count = pht( '%s / %s', new PhutilNumber($health_up), new PhutilNumber($health_up + $health_down)); $health_status = array( $health_icon, ' ', $health_count, ); $conn_message = $database->getConnectionMessage(); if ($conn_message) { $messages[] = $conn_message; } $replica_message = $database->getReplicaMessage(); if ($replica_message) { $messages[] = $replica_message; } $messages = phutil_implode_html(phutil_tag('br'), $messages); $partition = null; if ($database->getIsMaster()) { if ($database->getIsDefaultPartition()) { $partition = id(new PHUIIconView()) ->setIcon('fa-circle sky') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Default Partition'), )); } else { $map = $database->getApplicationMap(); if ($map) { $list = implode(', ', $map); } else { $list = pht('Empty'); } $partition = id(new PHUIIconView()) ->setIcon('fa-adjust sky') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Partition: %s', $list), )); } } $rows[] = array( $role_icon, $partition, $database->getHost(), $database->getPort(), $database->getUser(), $connection, $replication, $health_status, $messages, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString( pht('Phabricator is not configured in cluster mode.')) ->setHeaders( array( null, null, pht('Host'), pht('Port'), pht('User'), pht('Connection'), pht('Replication'), pht('Health'), pht('Messages'), )) ->setColumnClasses( array( null, null, null, null, null, null, null, null, 'wide', )); return $table; } } diff --git a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php index d0d26d0613..47ab022831 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php @@ -1,164 +1,164 @@ buildSideNavView(); $nav->selectFilter('cluster/notifications/'); $title = pht('Cluster Notifications'); $doc_href = PhabricatorEnv::getDoclink('Cluster: Notifications'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true) ->addActionLink( id(new PHUIButtonView()) ->setIcon('fa-book') ->setHref($doc_href) ->setTag('a') ->setText(pht('Documentation'))); $crumbs = $this - ->buildApplicationCrumbs($nav) + ->buildApplicationCrumbs() ->addTextCrumb($title) ->setBorder(true); $notification_status = $this->buildClusterNotificationStatus(); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent($notification_status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } private function buildClusterNotificationStatus() { $viewer = $this->getViewer(); $servers = PhabricatorNotificationServerRef::newRefs(); Javelin::initBehavior('phabricator-tooltips'); $rows = array(); foreach ($servers as $server) { if ($server->isAdminServer()) { $type_icon = 'fa-database sky'; $type_tip = pht('Admin Server'); } else { $type_icon = 'fa-bell sky'; $type_tip = pht('Client Server'); } $type_icon = id(new PHUIIconView()) ->setIcon($type_icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $type_tip, )); $messages = array(); $details = array(); if ($server->isAdminServer()) { try { $details = $server->loadServerStatus(); $status_icon = 'fa-exchange green'; $status_label = pht('Version %s', idx($details, 'version')); } catch (Exception $ex) { $status_icon = 'fa-times red'; $status_label = pht('Connection Error'); $messages[] = $ex->getMessage(); } } else { try { $server->testClient(); $status_icon = 'fa-exchange green'; $status_label = pht('Connected'); } catch (Exception $ex) { $status_icon = 'fa-times red'; $status_label = pht('Connection Error'); $messages[] = $ex->getMessage(); } } if ($details) { $uptime = idx($details, 'uptime'); $uptime = $uptime / 1000; $uptime = phutil_format_relative_time_detailed($uptime); $clients = pht( '%s Active / %s Total', new PhutilNumber(idx($details, 'clients.active')), new PhutilNumber(idx($details, 'clients.total'))); $stats = pht( '%s In / %s Out', new PhutilNumber(idx($details, 'messages.in')), new PhutilNumber(idx($details, 'messages.out'))); } else { $uptime = null; $clients = null; $stats = null; } $status_view = array( id(new PHUIIconView())->setIcon($status_icon), ' ', $status_label, ); $messages = phutil_implode_html(phutil_tag('br'), $messages); $rows[] = array( $type_icon, $server->getProtocol(), $server->getHost(), $server->getPort(), $status_view, $uptime, $clients, $stats, $messages, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString( pht('No notification servers are configured.')) ->setHeaders( array( null, pht('Proto'), pht('Host'), pht('Port'), pht('Status'), pht('Uptime'), pht('Clients'), pht('Messages'), null, )) ->setColumnClasses( array( null, null, null, null, null, null, null, null, 'wide', )); return $table; } } diff --git a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php index b3fd9915b1..3531c916a4 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php @@ -1,420 +1,420 @@ buildSideNavView(); $nav->selectFilter('cluster/repositories/'); $title = pht('Cluster Repository Status'); $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true) ->addActionLink( id(new PHUIButtonView()) ->setIcon('fa-book') ->setHref($doc_href) ->setTag('a') ->setText(pht('Documentation'))); $crumbs = $this - ->buildApplicationCrumbs($nav) + ->buildApplicationCrumbs() ->addTextCrumb(pht('Repository Servers')) ->setBorder(true); $repository_status = $this->buildClusterRepositoryStatus(); $repository_errors = $this->buildClusterRepositoryErrors(); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent( array( $repository_status, $repository_errors, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } private function buildClusterRepositoryStatus() { $viewer = $this->getViewer(); Javelin::initBehavior('phabricator-tooltips'); $all_services = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withServiceTypes( array( AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->needBindings(true) ->needProperties(true) ->execute(); $all_services = mpull($all_services, null, 'getPHID'); $all_repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withTypes( array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT, )) ->execute(); $all_repositories = mpull($all_repositories, null, 'getPHID'); $all_versions = id(new PhabricatorRepositoryWorkingCopyVersion()) ->loadAll(); $all_devices = $this->getDevices($all_services, false); $all_active_devices = $this->getDevices($all_services, true); $leader_versions = $this->getLeaderVersionsByRepository( $all_repositories, $all_versions, $all_active_devices); $push_times = $this->loadLeaderPushTimes($leader_versions); $repository_groups = mgroup($all_repositories, 'getAlmanacServicePHID'); $repository_versions = mgroup($all_versions, 'getRepositoryPHID'); $rows = array(); foreach ($all_services as $service) { $service_phid = $service->getPHID(); if ($service->getAlmanacPropertyValue('closed')) { $status_icon = 'fa-folder'; $status_tip = pht('Closed'); } else { $status_icon = 'fa-folder-open green'; $status_tip = pht('Open'); } $status_icon = id(new PHUIIconView()) ->setIcon($status_icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $status_tip, )); $devices = idx($all_devices, $service_phid, array()); $active_devices = idx($all_active_devices, $service_phid, array()); $device_icon = 'fa-server green'; $device_label = pht( '%s Active', phutil_count($active_devices)); $device_status = array( id(new PHUIIconView())->setIcon($device_icon), ' ', $device_label, ); $repositories = idx($repository_groups, $service_phid, array()); $repository_status = pht( '%s', phutil_count($repositories)); $no_leader = array(); $full_sync = array(); $partial_sync = array(); $no_sync = array(); $lag = array(); // Threshold in seconds before we start complaining that repositories // are not synchronized when there is only one leader. $threshold = phutil_units('5 minutes in seconds'); $messages = array(); foreach ($repositories as $repository) { $repository_phid = $repository->getPHID(); $leader_version = idx($leader_versions, $repository_phid); if ($leader_version === null) { $no_leader[] = $repository; $messages[] = pht( 'Repository %s has an ambiguous leader.', $viewer->renderHandle($repository_phid)->render()); continue; } $versions = idx($repository_versions, $repository_phid, array()); // Filter out any versions for devices which are no longer active. foreach ($versions as $key => $version) { $version_device_phid = $version->getDevicePHID(); if (empty($active_devices[$version_device_phid])) { unset($versions[$key]); } } $leaders = 0; foreach ($versions as $version) { if ($version->getRepositoryVersion() == $leader_version) { $leaders++; } } if ($leaders == count($active_devices)) { $full_sync[] = $repository; } else { $push_epoch = idx($push_times, $repository_phid); if ($push_epoch) { $duration = (PhabricatorTime::getNow() - $push_epoch); $lag[] = $duration; } else { $duration = null; } if ($leaders >= 2 || ($duration && ($duration < $threshold))) { $partial_sync[] = $repository; } else { $no_sync[] = $repository; if ($push_epoch) { $messages[] = pht( 'Repository %s has unreplicated changes (for %s).', $viewer->renderHandle($repository_phid)->render(), phutil_format_relative_time($duration)); } else { $messages[] = pht( 'Repository %s has unreplicated changes.', $viewer->renderHandle($repository_phid)->render()); } } } } $with_lag = false; if ($no_leader) { $replication_icon = 'fa-times red'; $replication_label = pht('Ambiguous Leader'); } else if ($no_sync) { $replication_icon = 'fa-refresh yellow'; $replication_label = pht('Unsynchronized'); $with_lag = true; } else if ($partial_sync) { $replication_icon = 'fa-refresh green'; $replication_label = pht('Partial'); $with_lag = true; } else if ($full_sync) { $replication_icon = 'fa-check green'; $replication_label = pht('Synchronized'); } else { $replication_icon = 'fa-times grey'; $replication_label = pht('No Repositories'); } if ($with_lag && $lag) { $lag_status = phutil_format_relative_time(max($lag)); $lag_status = pht(' (%s)', $lag_status); } else { $lag_status = null; } $replication_status = array( id(new PHUIIconView())->setIcon($replication_icon), ' ', $replication_label, $lag_status, ); $messages = phutil_implode_html(phutil_tag('br'), $messages); $rows[] = array( $status_icon, $viewer->renderHandle($service->getPHID()), $device_status, $repository_status, $replication_status, $messages, ); } return id(new AphrontTableView($rows)) ->setNoDataString( pht('No repository cluster services are configured.')) ->setHeaders( array( null, pht('Service'), pht('Devices'), pht('Repos'), pht('Sync'), pht('Messages'), )) ->setColumnClasses( array( null, 'pri', null, null, null, 'wide', )); } private function getDevices( array $all_services, $only_active) { $devices = array(); foreach ($all_services as $service) { $map = array(); foreach ($service->getBindings() as $binding) { if ($only_active && $binding->getIsDisabled()) { continue; } $device = $binding->getDevice(); $device_phid = $device->getPHID(); $map[$device_phid] = $device; } $devices[$service->getPHID()] = $map; } return $devices; } private function getLeaderVersionsByRepository( array $all_repositories, array $all_versions, array $active_devices) { $version_map = mgroup($all_versions, 'getRepositoryPHID'); $result = array(); foreach ($all_repositories as $repository_phid => $repository) { $service_phid = $repository->getAlmanacServicePHID(); if (!$service_phid) { continue; } $devices = idx($active_devices, $service_phid); if (!$devices) { continue; } $versions = idx($version_map, $repository_phid, array()); $versions = mpull($versions, null, 'getDevicePHID'); $versions = array_select_keys($versions, array_keys($devices)); if (!$versions) { continue; } $leader = (int)max(mpull($versions, 'getRepositoryVersion')); $result[$repository_phid] = $leader; } return $result; } private function loadLeaderPushTimes(array $leader_versions) { $viewer = $this->getViewer(); if (!$leader_versions) { return array(); } $events = id(new PhabricatorRepositoryPushEventQuery()) ->setViewer($viewer) ->withIDs($leader_versions) ->execute(); $events = mpull($events, null, 'getID'); $result = array(); foreach ($leader_versions as $key => $version) { $event = idx($events, $version); if (!$event) { continue; } $result[$key] = $event->getEpoch(); } return $result; } private function buildClusterRepositoryErrors() { $viewer = $this->getViewer(); $messages = id(new PhabricatorRepositoryStatusMessage())->loadAllWhere( 'statusCode IN (%Ls)', array( PhabricatorRepositoryStatusMessage::CODE_ERROR, )); $repository_ids = mpull($messages, 'getRepositoryID'); if ($repository_ids) { // NOTE: We're bypassing policies when loading repositories because we // want to show errors exist even if the viewer can't see the repository. // We use handles to describe the repository below, so the viewer won't // actually be able to see any particulars if they can't see the // repository. $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs($repository_ids) ->execute(); $repositories = mpull($repositories, null, 'getID'); } $rows = array(); foreach ($messages as $message) { $repository = idx($repositories, $message->getRepositoryID()); if (!$repository) { continue; } if (!$repository->isTracked()) { continue; } $icon = id(new PHUIIconView()) ->setIcon('fa-exclamation-triangle red'); $rows[] = array( $icon, $viewer->renderHandle($repository->getPHID()), phutil_tag( 'a', array( 'href' => $repository->getPathURI('manage/status/'), ), $message->getStatusTypeName()), ); } return id(new AphrontTableView($rows)) ->setNoDataString( pht('No active repositories have outstanding errors.')) ->setHeaders( array( null, pht('Repository'), pht('Error'), )) ->setColumnClasses( array( null, 'pri', 'wide', )); } } diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index 7e4ee758f6..869f53c223 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -1,119 +1,119 @@ getViewer(); $nav = $this->buildSideNavView(); $nav->selectFilter('issue/'); $engine = new PhabricatorSetupEngine(); $response = $engine->execute(); if ($response) { return $response; } $issues = $engine->getIssues(); $important = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_IMPORTANT, 'fa-warning'); $php = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_PHP, 'fa-code'); $mysql = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_MYSQL, 'fa-database'); $other = $this->buildIssueList( $issues, PhabricatorSetupCheck::GROUP_OTHER, 'fa-question-circle'); $no_issues = null; if (empty($issues)) { $no_issues = id(new PHUIInfoView()) ->setTitle(pht('No Issues')) ->appendChild( pht('Your install has no current setup issues to resolve.')) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); } $title = pht('Setup Issues'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true); $crumbs = $this - ->buildApplicationCrumbs($nav) + ->buildApplicationCrumbs() ->addTextCrumb(pht('Setup Issues')) ->setBorder(true); $page = array( $no_issues, $important, $php, $mysql, $other, ); $content = id(new PhabricatorConfigPageView()) ->setHeader($header) ->setContent($page); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($content) ->addClass('white-background'); } private function buildIssueList(array $issues, $group, $fonticon) { assert_instances_of($issues, 'PhabricatorSetupIssue'); $list = new PHUIObjectItemListView(); $list->setBig(true); $ignored_items = array(); $items = 0; foreach ($issues as $issue) { if ($issue->getGroup() == $group) { $items++; $href = $this->getApplicationURI('/issue/'.$issue->getIssueKey().'/'); $item = id(new PHUIObjectItemView()) ->setHeader($issue->getName()) ->setHref($href) ->addAttribute($issue->getSummary()); if (!$issue->getIsIgnored()) { $icon = id(new PHUIIconView()) ->setIcon($fonticon) ->setBackground('bg-sky'); $item->setImageIcon($icon); $list->addItem($item); } else { $icon = id(new PHUIIconView()) ->setIcon('fa-eye-slash') ->setBackground('bg-grey'); $item->setDisabled(true); $item->setImageIcon($icon); $ignored_items[] = $item; } } } foreach ($ignored_items as $item) { $list->addItem($item); } if ($items == 0) { return null; } else { return $list; } } } diff --git a/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php b/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php index 41133d5ede..75d3e8ad6d 100644 --- a/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php +++ b/src/applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php @@ -1,153 +1,153 @@ newOption( 'syntax-highlighter.engine', 'class', 'PhutilDefaultSyntaxHighlighterEngine') ->setBaseClass('PhutilSyntaxHighlighterEngine') ->setSummary(pht('Default non-pygments syntax highlighter engine.')) ->setDescription( pht( 'Phabricator can highlight PHP by default and use Pygments for '. 'other languages if enabled. You can provide a custom '. 'highlighter engine by extending class %s.', 'PhutilSyntaxHighlighterEngine')), $this->newOption('pygments.enabled', 'bool', false) ->setSummary( pht('Should Phabricator use Pygments to highlight code?')) ->setBoolOptions( array( pht('Use Pygments'), pht('Do Not Use Pygments'), )) ->setDescription( pht( 'Phabricator supports syntax highlighting a few languages by '. 'default, but you can install Pygments (a third-party syntax '. 'highlighting tool) to provide support for many more languages.'. "\n\n". 'To install Pygments, visit '. '[[ http://pygments.org | pygments.org ]] and follow the '. 'download and install instructions.'. "\n\n". 'Once Pygments is installed, enable this option '. '(`pygments.enabled`) to make Phabricator use Pygments when '. 'highlighting source code.'. "\n\n". 'After you install and enable Pygments, newly created source '. 'code (like diffs and pastes) should highlight correctly. '. 'You may need to clear Phabricator\'s caches to get previously '. 'existing source code to highlight. For instructions on '. 'managing caches, see [[ %s | Managing Caches ]].', $caches_href)), $this->newOption( 'pygments.dropdown-choices', 'wild', array( 'apacheconf' => 'Apache Configuration', 'bash' => 'Bash Scripting', 'brainfuck' => 'Brainf*ck', 'c' => 'C', 'coffee-script' => 'CoffeeScript', 'cpp' => 'C++', 'csharp' => 'C#', 'css' => 'CSS', 'd' => 'D', 'diff' => 'Diff', 'django' => 'Django Templating', 'docker' => 'Docker', 'erb' => 'Embedded Ruby/ERB', 'erlang' => 'Erlang', 'go' => 'Golang', 'groovy' => 'Groovy', 'haskell' => 'Haskell', 'html' => 'HTML', 'http' => 'HTTP', 'invisible' => 'Invisible', 'java' => 'Java', 'js' => 'Javascript', 'json' => 'JSON', 'make' => 'Makefile', 'mysql' => 'MySQL', 'nginx' => 'Nginx Configuration', 'objc' => 'Objective-C', 'perl' => 'Perl', 'php' => 'PHP', 'postgresql' => 'PostgreSQL', 'pot' => 'Gettext Catalog', 'puppet' => 'Puppet', 'python' => 'Python', 'rainbow' => 'Rainbow', 'remarkup' => 'Remarkup', 'rst' => 'reStructuredText', 'robotframework' => 'RobotFramework', 'ruby' => 'Ruby', 'sql' => 'SQL', 'tex' => 'LaTeX', 'text' => 'Plain Text', 'twig' => 'Twig', 'xml' => 'XML', 'yaml' => 'YAML', )) ->setSummary( pht('Set the language list which appears in dropdowns.')) ->setDescription( pht( 'In places that we display a dropdown to syntax-highlight code, '. 'this is where that list is defined.')), $this->newOption( 'syntax.filemap', 'wild', array( '@\.arcconfig$@' => 'js', '@\.arclint$@' => 'js', '@\.divinerconfig$@' => 'js', )) ->setSummary( pht('Override what language files (based on filename) highlight as.')) ->setDescription( pht( 'This is an override list of regular expressions which allows '. 'you to choose what language files are highlighted as. If your '. 'projects have certain rules about filenames or use unusual or '. 'ambiguous language extensions, you can create a mapping here. '. 'This is an ordered dictionary of regular expressions which will '. 'be tested against the filename. They should map to either an '. 'explicit language as a string value, or a numeric index into '. 'the captured groups as an integer.')) ->addExample('{"@\\.xyz$@": "php"}', pht('Highlight %s as PHP.', '*.xyz')) ->addExample( '{"@/httpd\\.conf@": "apacheconf"}', pht('Highlight httpd.conf as "apacheconf".')) ->addExample( '{"@\\.([^.]+)\\.bak$@": 1}', pht( "Treat all '*.x.bak' file as '.x'. NOTE: We map to capturing group ". "1 by specifying the mapping as '1'")), ); } } diff --git a/src/applications/config/schema/PhabricatorConfigKeySchema.php b/src/applications/config/schema/PhabricatorConfigKeySchema.php index b30a3641e9..566df54601 100644 --- a/src/applications/config/schema/PhabricatorConfigKeySchema.php +++ b/src/applications/config/schema/PhabricatorConfigKeySchema.php @@ -1,114 +1,115 @@ indexType = $index_type; return $this; } public function getIndexType() { return $this->indexType; } public function setProperty($property) { $this->property = $property; return $this; } public function getProperty() { return $this->property; } public function setUnique($unique) { $this->unique = $unique; return $this; } public function getUnique() { return $this->unique; } public function setTable(PhabricatorConfigTableSchema $table) { $this->table = $table; return $this; } public function getTable() { return $this->table; } public function setColumnNames(array $column_names) { $this->columnNames = array_values($column_names); return $this; } public function getColumnNames() { return $this->columnNames; } protected function getSubschemata() { return array(); } public function getKeyColumnAndPrefix($column_name) { $matches = null; if (preg_match('/^(.*)\((\d+)\)\z/', $column_name, $matches)) { return array($matches[1], (int)$matches[2]); } else { return array($column_name, null); } } public function getKeyByteLength() { $size = 0; foreach ($this->getColumnNames() as $column_spec) { list($column_name, $prefix) = $this->getKeyColumnAndPrefix($column_spec); $column = $this->getTable()->getColumn($column_name); if (!$column) { $size = 0; break; } $size += $column->getKeyByteLength($prefix); } return $size; } protected function compareToSimilarSchema( PhabricatorConfigStorageSchema $expect) { $issues = array(); if ($this->getColumnNames() !== $expect->getColumnNames()) { $issues[] = self::ISSUE_KEYCOLUMNS; } if ($this->getUnique() !== $expect->getUnique()) { $issues[] = self::ISSUE_UNIQUE; } // A fulltext index can be of any length. if ($this->getIndexType() != 'FULLTEXT') { if ($this->getKeyByteLength() > self::MAX_INNODB_KEY_LENGTH) { $issues[] = self::ISSUE_LONGKEY; } } return $issues; } public function newEmptyClone() { $clone = clone $this; $this->table = null; return $clone; } } diff --git a/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php index 85f450a648..9cae87e10a 100644 --- a/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php @@ -1,70 +1,67 @@ 'required string', 'topic' => 'optional string', 'message' => 'optional string', 'participantPHIDs' => 'required list', ); } protected function defineReturnType() { return 'nonempty dict'; } protected function defineErrorTypes() { return array( 'ERR_EMPTY_PARTICIPANT_PHIDS' => pht( 'You must specify participant phids.'), 'ERR_EMPTY_TITLE' => pht( 'You must specify a title.'), ); } protected function execute(ConduitAPIRequest $request) { $participant_phids = $request->getValue('participantPHIDs', array()); $message = $request->getValue('message'); $title = $request->getValue('title'); $topic = $request->getValue('topic'); list($errors, $conpherence) = ConpherenceEditor::createThread( $request->getUser(), $participant_phids, $title, $message, $request->newContentSource(), $topic); if ($errors) { foreach ($errors as $error_code) { switch ($error_code) { - case ConpherenceEditor::ERROR_EMPTY_TITLE: - throw new ConduitException('ERR_EMPTY_TITLE'); - break; case ConpherenceEditor::ERROR_EMPTY_PARTICIPANTS: throw new ConduitException('ERR_EMPTY_PARTICIPANT_PHIDS'); break; } } } return array( 'conpherenceID' => $conpherence->getID(), 'conpherencePHID' => $conpherence->getPHID(), 'conpherenceURI' => $this->getConpherenceURI($conpherence), ); } } diff --git a/src/applications/dashboard/controller/PhabricatorDashboardManageController.php b/src/applications/dashboard/controller/PhabricatorDashboardManageController.php index 60b47d6361..39a7e46841 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardManageController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardManageController.php @@ -1,139 +1,139 @@ getViewer(); $id = $request->getURIData('id'); // TODO: This UI should drop a lot of capabilities if the user can't // edit the dashboard, but we should still let them in for "Install" and // "View History". $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needPanels(true) ->executeOne(); if (!$dashboard) { return new Aphront404Response(); } $this->setDashboard($dashboard); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $dashboard, PhabricatorPolicyCapability::CAN_EDIT); $title = $dashboard->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Manage')); $header = $this->buildHeaderView(); - $curtain = $this->buildCurtainview($dashboard); + $curtain = $this->buildCurtainView($dashboard); $properties = $this->buildPropertyView($dashboard); $timeline = $this->buildTransactionTimeline( $dashboard, new PhabricatorDashboardTransactionQuery()); $timeline->setShouldTerminate(true); $info_view = null; if (!$can_edit) { $no_edit = pht( 'You do not have permission to edit this dashboard.'); $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors(array($no_edit)); } $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $info_view, $properties, $timeline, )); $navigation = $this->buildSideNavView('manage'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); } private function buildCurtainView(PhabricatorDashboard $dashboard) { $viewer = $this->getViewer(); $id = $dashboard->getID(); $curtain = $this->newCurtainView($dashboard); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $dashboard, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Dashboard')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit)); if ($dashboard->isArchived()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Dashboard')) ->setIcon('fa-check') ->setHref($this->getApplicationURI("archive/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow($can_edit)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Dashboard')) ->setIcon('fa-ban') ->setHref($this->getApplicationURI("archive/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow($can_edit)); } return $curtain; } private function buildPropertyView(PhabricatorDashboard $dashboard) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $dashboard); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $properties->addProperty( pht('Panels'), $viewer->renderHandleList($dashboard->getPanelPHIDs())); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 26aa24cb0c..dfe328933a 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -1,358 +1,357 @@ dashboardID = $id; return $this; } public function getDashboardID() { return $this->dashboardID; } public function setHeaderMode($header_mode) { $this->headerMode = $header_mode; return $this; } public function getHeaderMode() { return $this->headerMode; } /** * Allow the engine to render the panel via Ajax. */ public function setEnableAsyncRendering($enable) { $this->enableAsyncRendering = $enable; return $this; } public function setParentPanelPHIDs(array $parents) { $this->parentPanelPHIDs = $parents; return $this; } public function getParentPanelPHIDs() { return $this->parentPanelPHIDs; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setPanel(PhabricatorDashboardPanel $panel) { $this->panel = $panel; return $this; } public function setMovable($movable) { $this->movable = $movable; return $this; } public function getMovable() { return $this->movable; } public function getPanel() { return $this->panel; } public function setPanelPHID($panel_phid) { $this->panelPHID = $panel_phid; return $this; } public function getPanelPHID() { return $this->panelPHID; } public function renderPanel() { $panel = $this->getPanel(); - $viewer = $this->getViewer(); if (!$panel) { return $this->renderErrorPanel( pht('Missing or Restricted Panel'), pht( 'This panel does not exist, or you do not have permission '. 'to see it.')); } $panel_type = $panel->getImplementation(); if (!$panel_type) { return $this->renderErrorPanel( $panel->getName(), pht( 'This panel has type "%s", but that panel type is not known to '. 'Phabricator.', $panel->getPanelType())); } try { $this->detectRenderingCycle($panel); if ($this->enableAsyncRendering) { if ($panel_type->shouldRenderAsync()) { return $this->renderAsyncPanel(); } } - return $this->renderNormalPanel($viewer, $panel, $this); + return $this->renderNormalPanel(); } catch (Exception $ex) { return $this->renderErrorPanel( $panel->getName(), pht( '%s: %s', phutil_tag('strong', array(), get_class($ex)), $ex->getMessage())); } } private function renderNormalPanel() { $panel = $this->getPanel(); $panel_type = $panel->getImplementation(); $content = $panel_type->renderPanelContent( $this->getViewer(), $panel, $this); $header = $this->renderPanelHeader(); return $this->renderPanelDiv( $content, $header); } private function renderAsyncPanel() { $panel = $this->getPanel(); $panel_id = celerity_generate_unique_node_id(); $dashboard_id = $this->getDashboardID(); Javelin::initBehavior( 'dashboard-async-panel', array( 'panelID' => $panel_id, 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 'headerMode' => $this->getHeaderMode(), 'dashboardID' => $dashboard_id, 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', )); $header = $this->renderPanelHeader(); $content = id(new PHUIPropertyListView()) ->addTextContent(pht('Loading...')); return $this->renderPanelDiv( $content, $header, $panel_id); } private function renderErrorPanel($title, $body) { switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIHeaderView()) ->setHeader($title); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($title); break; } $icon = id(new PHUIIconView()) ->setIcon('fa-warning red msr'); $content = id(new PHUIBoxView()) ->addClass('dashboard-box') ->addMargin(PHUI::MARGIN_MEDIUM) ->appendChild($icon) ->appendChild($body); return $this->renderPanelDiv( $content, $header); } private function renderPanelDiv( $content, $header = null, $id = null) { require_celerity_resource('phabricator-dashboard-css'); $panel = $this->getPanel(); if (!$id) { $id = celerity_generate_unique_node_id(); } $box = new PHUIObjectBoxView(); $interface = 'PhabricatorApplicationSearchResultView'; if ($content instanceof $interface) { if ($content->getObjectList()) { $box->setObjectList($content->getObjectList()); } if ($content->getTable()) { $box->setTable($content->getTable()); } if ($content->getContent()) { $box->appendChild($content->getContent()); } } else { $box->appendChild($content); } $box ->setHeader($header) ->setID($id) ->addClass('dashboard-box') ->addSigil('dashboard-panel'); if ($this->getMovable()) { $box->addSigil('panel-movable'); } if ($panel) { $box->setMetadata( array( 'objectPHID' => $panel->getPHID(), )); } return phutil_tag_div('dashboard-pane', $box); } private function renderPanelHeader() { $panel = $this->getPanel(); switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $panel_type = $panel->getImplementation(); $header = $panel_type->adjustPanelHeader( $this->getViewer(), $panel, $this, $header); break; } return $header; } private function addPanelHeaderActions( PHUIHeaderView $header) { $panel = $this->getPanel(); $dashboard_id = $this->getDashboardID(); if ($panel) { $panel_id = $panel->getID(); $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; $edit_uri = new PhutilURI($edit_uri); if ($dashboard_id) { $edit_uri->setQueryParam('dashboardID', $dashboard_id); } $action_edit = id(new PHUIIconView()) ->setIcon('fa-pencil') ->setWorkflow(true) ->setHref((string)$edit_uri); $header->addActionItem($action_edit); } if ($dashboard_id) { $panel_phid = $this->getPanelPHID(); $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; $remove_uri = id(new PhutilURI($remove_uri)) ->setQueryParam('panelPHID', $panel_phid); $action_remove = id(new PHUIIconView()) ->setIcon('fa-trash-o') ->setHref((string)$remove_uri) ->setWorkflow(true); $header->addActionItem($action_remove); } return $header; } /** * Detect graph cycles in panels, and deeply nested panels. * * This method throws if the current rendering stack is too deep or contains * a cycle. This can happen if you embed layout panels inside each other, * build a big stack of panels, or embed a panel in remarkup inside another * panel. Generally, all of this stuff is ridiculous and we just want to * shut it down. * * @param PhabricatorDashboardPanel Panel being rendered. * @return void */ private function detectRenderingCycle(PhabricatorDashboardPanel $panel) { if ($this->parentPanelPHIDs === null) { throw new PhutilInvalidStateException('setParentPanelPHIDs'); } $max_depth = 4; if (count($this->parentPanelPHIDs) >= $max_depth) { throw new Exception( pht( 'To render more than %s levels of panels nested inside other '. 'panels, purchase a subscription to Phabricator Gold.', new PhutilNumber($max_depth))); } if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) { throw new Exception( pht( 'You awake in a twisting maze of mirrors, all alike. '. 'You are likely to be eaten by a graph cycle. '. 'Should you escape alive, you resolve to be more careful about '. 'putting dashboard panels inside themselves.')); } } } diff --git a/src/applications/differential/customfield/DifferentialCustomField.php b/src/applications/differential/customfield/DifferentialCustomField.php index 487e412f66..0382034a0e 100644 --- a/src/applications/differential/customfield/DifferentialCustomField.php +++ b/src/applications/differential/customfield/DifferentialCustomField.php @@ -1,131 +1,131 @@ getFieldKey(); } // TODO: As above. public function getModernFieldKey() { return $this->getFieldKeyForConduit(); } protected function parseObjectList( $value, array $types, $allow_partial = false, array $suffixes = array()) { return id(new PhabricatorObjectListQuery()) ->setViewer($this->getViewer()) ->setAllowedTypes($types) ->setObjectList($value) ->setAllowPartialResults($allow_partial) ->setSuffixes($suffixes) ->execute(); } protected function renderObjectList( array $handles, array $suffixes = array()) { if (!$handles) { return null; } $out = array(); foreach ($handles as $handle) { $phid = $handle->getPHID(); if ($handle->getPolicyFiltered()) { $token = $phid; } else if ($handle->isComplete()) { $token = $handle->getCommandLineObjectName(); } $suffix = idx($suffixes, $phid); $token = $token.$suffix; $out[] = $token; } return implode(', ', $out); } public function getWarningsForDetailView() { if ($this->getProxy()) { return $this->getProxy()->getWarningsForDetailView(); } return array(); } public function getRequiredHandlePHIDsForRevisionHeaderWarnings() { return array(); } public function getWarningsForRevisionHeader(array $handles) { return array(); } /* -( Integration with Commit Messages )----------------------------------- */ /** * @task commitmessage */ public function getProTips() { if ($this->getProxy()) { return $this->getProxy()->getProTips(); } return array(); } /* -( Integration with Diff Properties )----------------------------------- */ /** * @task diff */ public function shouldAppearInDiffPropertyView() { if ($this->getProxy()) { return $this->getProxy()->shouldAppearInDiffPropertyView(); } return false; } /** * @task diff */ public function renderDiffPropertyViewLabel(DifferentialDiff $diff) { - if ($this->proxy) { - return $this->proxy->renderDiffPropertyViewLabel($diff); + if ($this->getProxy()) { + return $this->getProxy()->renderDiffPropertyViewLabel($diff); } return $this->getFieldName(); } /** * @task diff */ public function renderDiffPropertyViewValue(DifferentialDiff $diff) { - if ($this->proxy) { - return $this->proxy->renderDiffPropertyViewValue($diff); + if ($this->getProxy()) { + return $this->getProxy()->renderDiffPropertyViewValue($diff); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } } diff --git a/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php b/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php index b9fec508c5..64570bf2cc 100644 --- a/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php +++ b/src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php @@ -1,206 +1,206 @@ objects = $objects; - $phids = $query->getEvaluatedParameter('responsiblePHIDs', array()); + $phids = $query->getEvaluatedParameter('responsiblePHIDs'); if (!$phids) { throw new Exception( pht( 'You can not bucket results by required action without '. 'specifying "Responsible Users".')); } $phids = array_fuse($phids); $groups = array(); $groups[] = $this->newGroup() ->setName(pht('Must Review')) ->setKey(self::KEY_MUSTREVIEW) ->setNoDataString(pht('No revisions are blocked on your review.')) ->setObjects($this->filterMustReview($phids)); $groups[] = $this->newGroup() ->setName(pht('Ready to Review')) ->setKey(self::KEY_SHOULDREVIEW) ->setNoDataString(pht('No revisions are waiting on you to review them.')) ->setObjects($this->filterShouldReview($phids)); $groups[] = $this->newGroup() ->setName(pht('Ready to Land')) ->setNoDataString(pht('No revisions are ready to land.')) ->setObjects($this->filterShouldLand($phids)); $groups[] = $this->newGroup() ->setName(pht('Ready to Update')) ->setNoDataString(pht('No revisions are waiting for updates.')) ->setObjects($this->filterShouldUpdate($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Review')) ->setNoDataString(pht('None of your revisions are waiting on review.')) ->setObjects($this->filterWaitingForReview($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Authors')) ->setNoDataString(pht('No revisions are waiting on author action.')) ->setObjects($this->filterWaitingOnAuthors($phids)); // Because you can apply these buckets to queries which include revisions // that have been closed, add an "Other" bucket if we still have stuff // that didn't get filtered into any of the previous buckets. if ($this->objects) { $groups[] = $this->newGroup() ->setName(pht('Other Revisions')) ->setObjects($this->objects); } return $groups; } private function filterMustReview(array $phids) { $blocking = array( DifferentialReviewerStatus::STATUS_BLOCKING, DifferentialReviewerStatus::STATUS_REJECTED, DifferentialReviewerStatus::STATUS_REJECTED_OLDER, ); $blocking = array_fuse($blocking); $objects = $this->getRevisionsUnderReview($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if (!$this->hasReviewersWithStatus($object, $phids, $blocking)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterShouldReview(array $phids) { $reviewing = array( DifferentialReviewerStatus::STATUS_ADDED, DifferentialReviewerStatus::STATUS_COMMENTED, ); $reviewing = array_fuse($reviewing); $objects = $this->getRevisionsUnderReview($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if (!$this->hasReviewersWithStatus($object, $phids, $reviewing)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterShouldLand(array $phids) { $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; $objects = $this->getRevisionsAuthored($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if ($object->getStatus() != $status_accepted) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterShouldUpdate(array $phids) { $statuses = array( ArcanistDifferentialRevisionStatus::NEEDS_REVISION, ArcanistDifferentialRevisionStatus::CHANGES_PLANNED, ArcanistDifferentialRevisionStatus::IN_PREPARATION, ); $statuses = array_fuse($statuses); $objects = $this->getRevisionsAuthored($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if (empty($statuses[$object->getStatus()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingForReview(array $phids) { $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; $objects = $this->getRevisionsAuthored($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if ($object->getStatus() != $status_review) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingOnAuthors(array $phids) { $statuses = array( ArcanistDifferentialRevisionStatus::ACCEPTED, ArcanistDifferentialRevisionStatus::NEEDS_REVISION, ArcanistDifferentialRevisionStatus::CHANGES_PLANNED, ArcanistDifferentialRevisionStatus::IN_PREPARATION, ); $statuses = array_fuse($statuses); $objects = $this->getRevisionsNotAuthored($this->objects, $phids); $results = array(); foreach ($objects as $key => $object) { if (empty($statuses[$object->getStatus()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } } diff --git a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php index d8ce38b5d5..694d4bc012 100644 --- a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php @@ -1,239 +1,239 @@ 'required string', 'commit' => 'optional string', ); } protected function getResult(ConduitAPIRequest $request) { $result = parent::getResult($request); return array( 'changes' => mpull($result, 'toDictionary'), 'effectiveCommit' => $this->getEffectiveCommit($request), ); } protected function getGitResult(ConduitAPIRequest $request) { return $this->getGitOrMercurialResult($request); } protected function getMercurialResult(ConduitAPIRequest $request) { return $this->getGitOrMercurialResult($request); } /** * NOTE: We have to work particularly hard for SVN as compared to other VCS. * That's okay but means this shares little code with the other VCS. */ protected function getSVNResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $effective_commit = $this->getEffectiveCommit($request); if (!$effective_commit) { return $this->getEmptyResult(); } $drequest = clone $drequest; $drequest->updateSymbolicCommit($effective_commit); $path_change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $path_changes = $path_change_query->loadChanges(); $path = null; foreach ($path_changes as $change) { if ($change->getPath() == $drequest->getPath()) { $path = $change; } } if (!$path) { return $this->getEmptyResult(); } $change_type = $path->getChangeType(); switch ($change_type) { case DifferentialChangeType::TYPE_MULTICOPY: case DifferentialChangeType::TYPE_DELETE: if ($path->getTargetPath()) { $old = array( $path->getTargetPath(), $path->getTargetCommitIdentifier(), ); } else { $old = array($path->getPath(), $path->getCommitIdentifier() - 1); } $old_name = $path->getPath(); $new_name = ''; $new = null; break; case DifferentialChangeType::TYPE_ADD: $old = null; $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = ''; $new_name = $path->getPath(); break; case DifferentialChangeType::TYPE_MOVE_HERE: case DifferentialChangeType::TYPE_COPY_HERE: $old = array( $path->getTargetPath(), $path->getTargetCommitIdentifier(), ); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getTargetPath(); $new_name = $path->getPath(); break; case DifferentialChangeType::TYPE_MOVE_AWAY: $old = array( $path->getPath(), $path->getCommitIdentifier() - 1, ); $old_name = $path->getPath(); $new_name = null; $new = null; break; default: $old = array($path->getPath(), $path->getCommitIdentifier() - 1); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getPath(); $new_name = $path->getPath(); break; } $futures = array( 'old' => $this->buildSVNContentFuture($old), 'new' => $this->buildSVNContentFuture($new), ); $futures = array_filter($futures); foreach (new FutureIterator($futures) as $key => $future) { $stdout = ''; try { list($stdout) = $future->resolvex(); } catch (CommandException $e) { if ($path->getFileType() != DifferentialChangeType::FILE_DIRECTORY) { throw $e; } } $futures[$key] = $stdout; } $old_data = idx($futures, 'old', ''); $new_data = idx($futures, 'new', ''); $engine = new PhabricatorDifferenceEngine(); $engine->setOldName($old_name); $engine->setNewName($new_name); $raw_diff = $engine->generateRawDiffFromFileContent($old_data, $new_data); $arcanist_changes = DiffusionPathChange::convertToArcanistChanges( $path_changes); $parser = $this->getDefaultParser(); $parser->setChanges($arcanist_changes); $parser->forcePath($path->getPath()); $changes = $parser->parseDiff($raw_diff); $change = $changes[$path->getPath()]; return array($change); } private function getEffectiveCommit(ConduitAPIRequest $request) { if ($this->effectiveCommit === null) { $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $result = DiffusionQuery::callConduitWithDiffusionRequest( $request->getUser(), $drequest, 'diffusion.lastmodifiedquery', array( 'paths' => array($path => $drequest->getStableCommit()), )); $this->effectiveCommit = idx($result, $path); } return $this->effectiveCommit; } private function buildSVNContentFuture($spec) { if (!$spec) { return null; } $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); list($ref, $rev) = $spec; return $repository->getRemoteCommandFuture( 'cat %s', $repository->getSubversionPathURI($ref, $rev)); } private function getGitOrMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $effective_commit = $this->getEffectiveCommit($request); if (!$effective_commit) { - return $this->getEmptyResult(1); + return $this->getEmptyResult(); } $raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->setAnchorCommit($effective_commit); $raw_diff = $raw_query->executeInline(); if (!$raw_diff) { - return $this->getEmptyResult(2); + return $this->getEmptyResult(); } $parser = $this->getDefaultParser(); $changes = $parser->parseDiff($raw_diff); return $changes; } private function getDefaultParser() { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $parser = new ArcanistDiffParser(); $try_encoding = $repository->getDetail('encoding'); if ($try_encoding) { $parser->setTryEncoding($try_encoding); } $parser->setDetectBinaryFiles(true); return $parser; } private function getEmptyResult() { return array(); } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index f9d7b6792f..aa8e370e25 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1,2022 +1,2022 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); // Figure out if we're browsing a directory, a file, or a search result // list. $grep = $request->getStr('grep'); $find = $request->getStr('find'); if (strlen($grep) || strlen($find)) { return $this->browseSearch(); } $pager = id(new PHUIPagerView()) ->readFromRequest($request); $results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getStableCommit(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ))); $reason = $results->getReasonForEmptyResultSet(); $is_file = ($reason == DiffusionBrowseResultSet::REASON_IS_FILE); if ($is_file) { - return $this->browseFile($results); + return $this->browseFile(); } else { $paths = $results->getPaths(); $paths = $pager->sliceResults($paths); $results->setPaths($paths); return $this->browseDirectory($results, $pager); } } private function browseSearch() { $drequest = $this->getDiffusionRequest(); $header = $this->buildHeaderView($drequest); $search_form = $this->renderSearchForm(); $search_results = $this->renderSearchResults(); $search_form = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Search')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($search_form); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( $search_form, $search_results, )); return $this->newPage() ->setTitle( array( nonempty(basename($drequest->getPath()), '/'), $drequest->getRepository()->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild($view); } private function browseFile() { $viewer = $this->getViewer(); $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY; $show_blame = $request->getBool( 'blame', $viewer->getUserSetting($blame_key)); $view = $request->getStr('view'); if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) { $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($blame_key, $show_blame); $editor->applyTransactions($preferences, $xactions); $uri = $request->getRequestURI() ->alter('blame', null); return id(new AphrontRedirectResponse())->setURI($uri); } // We need the blame information if blame is on and this is an Ajax request. // If blame is on and this is a colorized request, we don't show blame at // first (we ajax it in afterward) so we don't need to query for it. $needs_blame = ($show_blame && $request->isAjax()); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), ); $byte_limit = null; if ($view !== 'raw') { $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold(); $time_limit = 10; $params += array( 'timeout' => $time_limit, 'byteLimit' => $byte_limit, ); } $response = $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', $params); $hit_byte_limit = $response['tooHuge']; $hit_time_limit = $response['tooSlow']; $file_phid = $response['filePHID']; if ($hit_byte_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file is larger than %s byte(s), and too large to display '. 'in the web UI.', phutil_format_bytes($byte_limit))); } else if ($hit_time_limit) { $corpus = $this->buildErrorCorpus( pht( 'This file took too long to load from the repository (more than '. '%s second(s)).', new PhutilNumber($time_limit))); } else { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception(pht('Failed to load content file!')); } if ($view === 'raw') { return $file->getRedirectResponse(); } $data = $file->loadFileData(); $lfs_ref = $this->getGitLFSRef($repository, $data); if ($lfs_ref) { if ($view == 'git-lfs') { $file = $this->loadGitLFSFile($lfs_ref); // Rename the file locally so we generate a better vanity URI for // it. In storage, it just has a name like "lfs-13f9a94c0923...", // since we don't get any hints about possible human-readable names // at upload time. $basename = basename($drequest->getPath()); $file->makeEphemeral(); $file->setName($basename); return $file->getRedirectResponse(); } else { $corpus = $this->buildGitLFSCorpus($lfs_ref); } } else if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $corpus = $this->buildImageCorpus($file_uri); } else { $corpus = $this->buildBinaryCorpus($file_uri, $data); } } else { $this->loadLintMessages(); $this->coverage = $drequest->loadCoverage(); // Build the content of the file. $corpus = $this->buildCorpus( $show_blame, $data, $needs_blame, $drequest, $path, $data); } } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($corpus); } require_celerity_resource('diffusion-source-css'); // Render the page. $view = $this->buildCurtain($drequest); $curtain = $this->enrichCurtain( $view, $drequest, $show_blame); $properties = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-file-code-o'); $content = array(); $follow = $request->getStr('follow'); if ($follow) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_WARNING); $notice->setTitle(pht('Unable to Continue')); switch ($follow) { case 'first': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit is the first commit in the repository.')); break; case 'created': $notice->appendChild( pht( 'Unable to continue tracing the history of this file because '. 'this commit created the file.')); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('File Renamed')); $notice->appendChild( pht( 'File history passes through a rename from "%s" to "%s".', $drequest->getPath(), $renamed)); $content[] = $notice; } $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $crumbs->setBorder(true); $basename = basename($this->getDiffusionRequest()->getPath()); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $content, )); if ($properties) { $view->addPropertySection(pht('Details'), $properties); } $title = array($basename, $repository->getDisplayName()); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } public function browseDirectory( DiffusionBrowseResultSet $results, PHUIPagerView $pager) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $reason = $results->getReasonForEmptyResultSet(); $curtain = $this->buildCurtain($drequest); $details = $this->buildPropertyView($drequest); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-folder-open'); $search_form = $this->renderSearchForm(); $empty_result = null; $browse_panel = null; $branch_panel = null; if (!$results->isValidResults()) { $empty_result = new DiffusionEmptyResultView(); $empty_result->setDiffusionRequest($drequest); $empty_result->setDiffusionBrowseResultSet($results); $empty_result->setView($request->getStr('view')); } else { $phids = array(); foreach ($results->getPaths() as $result) { $data = $result->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } } } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $browse_table = id(new DiffusionBrowseTableView()) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setPaths($results->getPaths()) ->setUser($request->getUser()); $browse_header = id(new PHUIHeaderView()) ->setHeader(nonempty(basename($drequest->getPath()), '/')) ->setHeaderIcon('fa-folder-open'); $browse_panel = id(new PHUIObjectBoxView()) ->setHeader($browse_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($browse_table); $browse_panel->setShowHide( array(pht('Show Search')), pht('Hide Search'), $search_form, '#'); $path = $drequest->getPath(); $is_branch = (!strlen($path) && $repository->supportsBranchComparison()); if ($is_branch) { $branch_panel = $this->buildBranchTable(); } } $open_revisions = $this->buildOpenRevisions(); $readme = $this->renderDirectoryReadme($results); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $pager_box = $this->renderTablePagerBox($pager); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $branch_panel, $empty_result, $browse_panel, )) ->setFooter( array( $open_revisions, $readme, $pager_box, )); if ($details) { $view->addPropertySection(pht('Details'), $details); } return $this->newPage() ->setTitle(array( nonempty(basename($drequest->getPath()), '/'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function renderSearchResults() { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $results = array(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $search_mode = null; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $results = array(); break; default: if (strlen($this->getRequest()->getStr('grep'))) { $search_mode = 'grep'; $query_string = $request->getStr('grep'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.searchquery', array( 'grep' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } else { // Filename search. $search_mode = 'find'; $query_string = $request->getStr('find'); $results = $this->callConduitWithDiffusionRequest( 'diffusion.querypaths', array( 'pattern' => $query_string, 'commit' => $drequest->getStableCommit(), 'path' => $drequest->getPath(), 'limit' => $pager->getPageSize() + 1, 'offset' => $pager->getOffset(), )); } break; } $results = $pager->sliceResults($results); if ($search_mode == 'grep') { $table = $this->renderGrepResults($results, $query_string); $header = pht( 'File content matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } else { $table = $this->renderFindResults($results); $header = pht( 'Paths matching "%s" under "%s"', $query_string, nonempty($drequest->getPath(), '/')); } $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); $pager_box = $this->renderTablePagerBox($pager); return array($box, $pager_box); } private function renderGrepResults(array $results, $pattern) { $drequest = $this->getDiffusionRequest(); require_celerity_resource('phabricator-search-results-css'); $rows = array(); foreach ($results as $result) { list($path, $line, $string) = $result; $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $path, 'line' => $line, )); $matches = null; $count = @preg_match_all( '('.$pattern.')u', $string, $matches, PREG_OFFSET_CAPTURE); if (!$count) { $output = ltrim($string); } else { $output = array(); $cursor = 0; $length = strlen($string); foreach ($matches[0] as $match) { $offset = $match[1]; if ($cursor != $offset) { $output[] = array( 'text' => substr($string, $cursor, $offset), 'highlight' => false, ); } $output[] = array( 'text' => $match[0], 'highlight' => true, ); $cursor = $offset + strlen($match[0]); } if ($cursor != $length) { $output[] = array( 'text' => substr($string, $cursor), 'highlight' => false, ); } if ($output) { $output[0]['text'] = ltrim($output[0]['text']); } foreach ($output as $key => $segment) { if ($segment['highlight']) { $output[$key] = phutil_tag('strong', array(), $segment['text']); } else { $output[$key] = $segment['text']; } } } $string = phutil_tag( 'pre', array('class' => 'PhabricatorMonospaced phui-source-fragment'), $output); $path = Filesystem::readablePath($path, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $path), $line, $string, ); } $table = id(new AphrontTableView($rows)) ->setClassName('remarkup-code') ->setHeaders(array(pht('Path'), pht('Line'), pht('String'))) ->setColumnClasses(array('', 'n', 'wide')) ->setNoDataString( pht( 'The pattern you searched for was not found in the content of any '. 'files.')); return $table; } private function renderFindResults(array $results) { $drequest = $this->getDiffusionRequest(); $rows = array(); foreach ($results as $result) { $href = $drequest->generateURI(array( 'action' => 'browse', 'path' => $result, )); $readable = Filesystem::readablePath($result, $drequest->getPath()); $rows[] = array( phutil_tag('a', array('href' => $href), $readable), ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array(pht('Path'))) ->setColumnClasses(array('wide')) ->setNoDataString( pht( 'The pattern you searched for did not match the names of any '. 'files.')); return $table; } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus( $show_blame, $file_corpus, $needs_blame, DiffusionRequest $drequest, $path, $data) { $viewer = $this->getViewer(); $blame_timeout = 15; $blame_failed = false; $highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $blame_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; $can_highlight = (strlen($file_corpus) <= $highlight_limit); $can_blame = (strlen($file_corpus) <= $blame_limit); if ($needs_blame && $can_blame) { $blame = $this->loadBlame($path, $drequest->getCommit(), $blame_timeout); list($blame_list, $blame_commits) = $blame; if ($blame_list === null) { $blame_failed = true; $blame_list = array(); } } else { $blame_list = array(); $blame_commits = array(); } require_celerity_resource('syntax-highlighting-css'); if ($can_highlight) { $highlighted = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $file_corpus); } else { // Highlight as plain text to escape the content properly. $highlighted = PhabricatorSyntaxHighlighter::highlightWithLanguage( 'txt', $file_corpus); } $lines = phutil_split_lines($highlighted); $rows = $this->buildDisplayRows( $lines, $blame_list, $blame_commits, $show_blame); $corpus_table = javelin_tag( 'table', array( 'class' => 'diffusion-source remarkup-code PhabricatorMonospaced', 'sigil' => 'phabricator-source', ), $rows); if ($this->getRequest()->isAjax()) { return $corpus_table; } $id = celerity_generate_unique_node_id(); $repo = $drequest->getRepository(); $symbol_repos = nonempty($repo->getSymbolSources(), array()); $symbol_repos[] = $repo->getPHID(); $lang = last(explode('.', $drequest->getPath())); $repo_languages = $repo->getSymbolLanguages(); $repo_languages = nonempty($repo_languages, array()); $repo_languages = array_fill_keys($repo_languages, true); $needs_symbols = true; if ($repo_languages && $symbol_repos) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repo->getPHID()); if (!$have_symbols) { $needs_symbols = false; } } if ($needs_symbols && $repo_languages) { $needs_symbols = isset($repo_languages[$lang]); } if ($needs_symbols) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'repositories' => $symbol_repos, )); } $corpus = phutil_tag( 'div', array( 'id' => $id, ), $corpus_table); Javelin::initBehavior('load-blame', array('id' => $id)); $edit = $this->renderEditButton(); $file = $this->renderFileButton(); $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->setHeaderIcon('fa-file-code-o') ->addActionLink($edit) ->addActionLink($file); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($corpus) ->setCollapsed(true); $messages = array(); if (!$can_highlight) { $messages[] = pht( 'This file is larger than %s, so syntax highlighting is disabled '. 'by default.', phutil_format_bytes($highlight_limit)); } if ($show_blame && !$can_blame) { $messages[] = pht( 'This file is larger than %s, so blame is disabled.', phutil_format_bytes($blame_limit)); } if ($blame_failed) { $messages[] = pht( 'Failed to load blame information for this file in %s second(s).', new PhutilNumber($blame_timeout)); } if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($messages)); } return $corpus; } private function enrichCurtain( PHUICurtainView $curtain, DiffusionRequest $drequest, $show_blame) { $viewer = $this->getViewer(); $base_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Last Change')) ->setHref( $drequest->generateURI( array( 'action' => 'change', ))) ->setIcon('fa-backward')); if ($show_blame) { $blame_text = pht('Disable Blame'); $blame_icon = 'fa-exclamation-circle lightgreytext'; $blame_value = 0; } else { $blame_text = pht('Enable Blame'); $blame_icon = 'fa-exclamation-circle'; $blame_value = 1; } $curtain->addAction( id(new PhabricatorActionView()) ->setName($blame_text) ->setHref($base_uri->alter('blame', $blame_value)) ->setIcon($blame_icon) ->setUser($viewer) ->setRenderAsForm($viewer->isLoggedIn())); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $curtain->addAction( id(new PhabricatorActionView()) ->setName($lint_text) ->setHref($href) ->setIcon('fa-exclamation-triangle') ->setDisabled(!$href)); $repository = $drequest->getRepository(); $owners = 'PhabricatorOwnersApplication'; if (PhabricatorApplication::isClassInstalled($owners)) { $package_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl( $repository->getPHID(), array( $drequest->getPath(), )); $package_query->execute(); $packages = $package_query->getControllingPackagesForPath( $repository->getPHID(), $drequest->getPath()); if ($packages) { $ownership = id(new PHUIStatusListView()) ->setUser($viewer); foreach ($packages as $package) { $icon = 'fa-list-alt'; $color = 'grey'; $item = id(new PHUIStatusItemView()) ->setIcon($icon, $color) ->setTarget($viewer->renderHandle($package->getPHID())); $ownership->addItem($item); } } else { $ownership = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Owners')) ->appendChild($ownership); } return $curtain; } private function renderEditButton() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $editor_link = $user->loadEditorLink($path, $line, $repository); $template = $user->loadEditorLink($path, '%l', $repository); $button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Open in Editor')) ->setHref($editor_link) ->setIcon('fa-pencil') ->setID('editor_link') ->setMetadata(array('link_template' => $template)) ->setDisabled(!$editor_link); return $button; } private function renderFileButton($file_uri = null, $label = null) { $base_uri = $this->getRequest()->getRequestURI(); if ($file_uri) { $text = pht('Download Raw File'); $href = $file_uri; $icon = 'fa-download'; } else { $text = pht('View Raw File'); $href = $base_uri->alter('view', 'raw'); $icon = 'fa-file-text'; } if ($label !== null) { $text = $label; } $button = id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); return $button; } private function renderGitLFSButton() { $viewer = $this->getViewer(); $uri = $this->getRequest()->getRequestURI(); $href = $uri->alter('view', 'git-lfs'); $text = pht('Download from Git LFS'); $icon = 'fa-download'; return id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($icon); } private function buildDisplayRows( array $lines, array $blame_list, array $blame_commits, $show_blame) { $request = $this->getRequest(); $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $revision_map = array(); $revisions = array(); if ($blame_commits) { $commit_map = mpull($blame_commits, 'getCommitIdentifier', 'getPHID'); $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(array_keys($commit_map)); if ($revision_ids) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs($revision_ids) ->execute(); $revisions = mpull($revisions, null, 'getID'); } foreach ($revision_ids as $commit_phid => $revision_id) { $revision_map[$commit_map[$commit_phid]] = $revision_id; } } $phids = array(); foreach ($blame_commits as $commit) { $author_phid = $commit->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } foreach ($revisions as $revision) { $author_phid = $revision->getAuthorPHID(); if ($author_phid === null) { continue; } $phids[$author_phid] = $author_phid; } $handles = $viewer->loadHandles($phids); $colors = array(); if ($blame_commits) { $epochs = array(); foreach ($blame_commits as $identifier => $commit) { $epochs[$identifier] = $commit->getEpoch(); } $epoch_list = array_filter($epochs); $epoch_list = array_unique($epoch_list); $epoch_list = array_values($epoch_list); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; foreach ($blame_commits as $identifier => $commit) { $epoch = $epochs[$identifier]; if (!$epoch) { $color = '#ffffdd'; // Warning color, missing data. } else { $color_ratio = ($epoch - $epoch_min) / $epoch_range; $color_value = 0xE6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $colors[$identifier] = $color; } } $display = array(); $last_identifier = null; $last_color = null; foreach ($lines as $line_index => $line) { $color = '#f6f6f6'; $duplicate = false; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; if (isset($colors[$identifier])) { $color = $colors[$identifier]; } if ($identifier === $last_identifier) { $duplicate = true; } else { $last_identifier = $identifier; } } $display[$line_index] = array( 'data' => $line, 'target' => false, 'highlighted' => false, 'color' => $color, 'duplicate' => $duplicate, ); } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } // Mark the first highlighted line as the target line. if ($line_arr) { $target_line = $line_arr[0]['min']; if (isset($display[$target_line - 1])) { $display[$target_line - 1]['target'] = true; } } // Mark all other highlighted lines as highlighted. foreach ($line_arr as $range) { for ($ii = $range['min']; $ii <= $range['max']; $ii++) { if (isset($display[$ii - 1])) { $display[$ii - 1]['highlighted'] = true; } } } $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $show_blame, (bool)$this->coverage, $engine); // NOTE: We're doing this manually because rendering is otherwise // dominated by URI generation for very large files. $line_base = (string)$drequest->generateURI( array( 'action' => 'browse', 'stable' => true, )); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-oncopy'); Javelin::initBehavior('phabricator-tooltips'); Javelin::initBehavior('phabricator-line-linker'); // Render these once, since they tend to get repeated many times in large // blame outputs. $commit_links = $this->renderCommitLinks($blame_commits, $handles); $revision_links = $this->renderRevisionLinks($revisions, $handles); if ($this->coverage) { require_celerity_resource('differential-changeset-view-css'); Javelin::initBehavior( 'diffusion-browse-file', array( 'labels' => array( 'cov-C' => pht('Covered'), 'cov-N' => pht('Not Covered'), 'cov-U' => pht('Not Executable'), ), )); } $skip_text = pht('Skip Past This Commit'); foreach ($display as $line_index => $line) { $row = array(); $line_number = $line_index + 1; $line_href = $line_base.'$'.$line_number; if (isset($blame_list[$line_index])) { $identifier = $blame_list[$line_index]; } else { $identifier = null; } $revision_link = null; $commit_link = null; $before_link = null; $style = 'background: '.$line['color'].';'; if ($identifier && !$line['duplicate']) { if (isset($commit_links[$identifier])) { $commit_link = $commit_links[$identifier]; } if (isset($revision_map[$identifier])) { $revision_id = $revision_map[$identifier]; if (isset($revision_links[$revision_id])) { $revision_link = $revision_links[$revision_id]; } } $skip_href = $line_href.'?before='.$identifier.'&view=blame'; $before_link = javelin_tag( 'a', array( 'href' => $skip_href, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $skip_text, 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } if ($show_blame) { $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', ), $before_link); $object_links = array(); $object_links[] = $commit_link; if ($revision_link) { $object_links[] = phutil_tag('span', array(), '/'); $object_links[] = $revision_link; } $row[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', ), $object_links); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, 'style' => $style, ), $line_number); $row[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'phabricator-source-line', 'style' => $style, ), $line_link); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $row[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html(str_replace("\t", ' ', $line['data'])), )); if ($this->coverage) { $cov_index = $line_index; if (isset($this->coverage[$cov_index])) { $cov_class = $this->coverage[$cov_index]; } else { $cov_class = 'N'; } $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-'.$cov_class, ), ''); } $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'phabricator-source-highlight' : null), ), $row); $cur_inlines = $this->renderInlines( idx($inlines, $line_number, array()), $show_blame, $this->coverage, $engine); foreach ($cur_inlines as $cur_inline) { $rows[] = $cur_inline; } } return $rows; } private function renderInlines( array $inlines, $show_blame, $has_coverage, $engine) { $rows = array(); foreach ($inlines as $inline) { // TODO: This should use modern scaffolding code. $inline_view = id(new PHUIDiffInlineCommentDetailView()) ->setUser($this->getViewer()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($show_blame ? 3 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); if ($has_coverage) { $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-I', )); } $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function buildImageCorpus($file_uri) { $properties = new PHUIPropertyListView(); $properties->addImageContent( phutil_tag( 'img', array( 'src' => $file_uri, ))); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->addActionLink($file) ->setHeaderIcon('fa-file-image-o'); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } private function buildBinaryCorpus($file_uri, $data) { $size = new PhutilNumber(strlen($data)); $text = pht('This is a binary file. It is %s byte(s) in length.', $size); $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($text); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')) ->addActionLink($file); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($text); return $box; } private function buildErrorCorpus($message) { $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($message); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($text); return $box; } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentCommitOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentCommitOf($parent); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent; } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $line = $drequest->getLine(); if (!$line) { return null; } $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'againstCommit' => $target_commit, )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery.')); } $raw_diff = $file->loadFileData(); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } private function loadParentCommitOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); $before_req = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( $user, $before_req, 'diffusion.commitparentsquery', array( 'commit' => $commit, )); return head($parents); } private function renderRevisionTooltip( DifferentialRevision $revision, $handles) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($revision->getDateModified(), $viewer); $id = $revision->getID(); $title = $revision->getTitle(); $header = "D{$id} {$title}"; $author = $handles[$revision->getAuthorPHID()]->getName(); return "{$header}\n{$date} \xC2\xB7 {$author}"; } private function renderCommitTooltip( PhabricatorRepositoryCommit $commit, $author) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($commit->getEpoch(), $viewer); $summary = trim($commit->getSummary()); return "{$summary}\n{$date} \xC2\xB7 {$author}"; } protected function renderSearchForm() { $drequest = $this->getDiffusionRequest(); $forms = array(); $form = id(new AphrontFormView()) ->setUser($this->getViewer()) ->setMethod('GET'); switch ($drequest->getRepository()->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $forms[] = id(clone $form) ->appendChild(pht('Search is not available in Subversion.')); break; default: $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('File Name')) ->setSubmitLabel(pht('Search File Names')) ->setName('find') ->setValue($this->getRequest()->getStr('find'))); $forms[] = id(clone $form) ->appendChild( id(new AphrontFormTextWithSubmitControl()) ->setLabel(pht('Pattern')) ->setSubmitLabel(pht('Grep File Content')) ->setName('grep') ->setValue($this->getRequest()->getStr('grep'))); break; } require_celerity_resource('diffusion-icons-css'); $form_box = phutil_tag_div('diffusion-search-boxen', $forms); return $form_box; } protected function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); $text = $engine->markupText($text); $text = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $text); return $text; } protected function buildHeaderView(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($this->renderPathLinks($drequest, $mode = 'browse')) ->addTag($tag); return $header; } protected function buildCurtain(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $repository = $drequest->getRepository(); $curtain = $this->newCurtainView($drequest); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-list')); $behind_head = $drequest->getSymbolicCommit(); if ($repository->supportsBranchComparison()) { $compare_uri = $drequest->generateURI( array( 'action' => 'compare', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Compare Against...')) ->setIcon('fa-code-fork') ->setWorkflow(true) ->setHref($compare_uri)); } $head_uri = $drequest->generateURI( array( 'commit' => '', 'action' => 'browse', )); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Jump to HEAD')) ->setHref($head_uri) ->setIcon('fa-home') ->setDisabled(!$behind_head)); return $curtain; } protected function buildPropertyView( DiffusionRequest $drequest) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); if ($drequest->getSymbolicType() == 'tag') { $symbolic = $drequest->getSymbolicCommit(); $view->addProperty(pht('Tag'), $symbolic); $tags = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( 'names' => array($symbolic), 'needMessages' => true, )); $tags = DiffusionRepositoryTag::newFromConduit($tags); $tags = mpull($tags, null, 'getName'); $tag = idx($tags, $symbolic); if ($tag && strlen($tag->getMessage())) { $view->addSectionHeader( pht('Tag Content'), 'fa-tag'); $view->addTextContent($this->markupText($tag->getMessage())); } } if ($view->hasAnyProperties()) { return $view; } return null; } private function buildOpenRevisions() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs(); $path_id = idx($path_map, $path); if (!$path_id) { return null; } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withPath($repository->getID(), $path_id) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needRelationships(true) ->needFlags(true) ->needDrafts(true) ->execute(); if (!$revisions) { return null; } $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Revisions')) ->setSubheader( pht('Recently updated open revisions affecting this file.')); $view = id(new DifferentialRevisionListView()) ->setHeader($header) ->setRevisions($revisions) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } private function loadBlame($path, $commit, $timeout) { $blame = $this->callConduitWithDiffusionRequest( 'diffusion.blame', array( 'commit' => $commit, 'paths' => array($path), 'timeout' => $timeout, )); $identifiers = idx($blame, $path, null); if ($identifiers) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($identifiers) // TODO: We only fetch this to improve author display behavior, but // shouldn't really need to? ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } else { $commits = array(); } return array($identifiers, $commits); } private function renderCommitLinks(array $commits, $handles) { $links = array(); foreach ($commits as $identifier => $commit) { $tooltip = $this->renderCommitTooltip( $commit, $commit->renderAuthorShortName($handles)); $commit_link = javelin_tag( 'a', array( 'href' => $commit->getURI(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $commit->getLocalName()); $links[$identifier] = $commit_link; } return $links; } private function renderRevisionLinks(array $revisions, $handles) { $links = array(); foreach ($revisions as $revision) { $revision_id = $revision->getID(); $tooltip = $this->renderRevisionTooltip($revision, $handles); $revision_link = javelin_tag( 'a', array( 'href' => '/'.$revision->getMonogram(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), $revision->getMonogram()); $links[$revision_id] = $revision_link; } return $links; } private function getGitLFSRef(PhabricatorRepository $repository, $data) { if (!$repository->canUseGitLFS()) { return null; } $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])'; if (!preg_match($lfs_pattern, $data)) { return null; } $matches = null; if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) { return null; } $hash = $matches[1]; $hash = trim($hash); return id(new PhabricatorRepositoryGitLFSRefQuery()) ->setViewer($this->getViewer()) ->withRepositoryPHIDs(array($repository->getPHID())) ->withObjectHashes(array($hash)) ->executeOne(); } private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef $ref) { // TODO: We should probably test if we can load the file PHID here and // show the user an error if we can't, rather than making them click // through to hit an error. $header = id(new PHUIHeaderView()) ->setHeader(basename($this->getDiffusionRequest()->getPath())) ->setHeaderIcon('fa-archive'); $severity = PHUIInfoView::SEVERITY_NOTICE; $messages = array(); $messages[] = pht( 'This %s file is stored in Git Large File Storage.', phutil_format_bytes($ref->getByteSize())); try { $file = $this->loadGitLFSFile($ref); $data = $this->renderGitLFSButton(); $header->addActionLink($data); } catch (Exception $ex) { $severity = PHUIInfoView::SEVERITY_ERROR; $messages[] = pht('The data for this file could not be loaded.'); } $raw = $this->renderFileButton(null, pht('View Raw LFS Pointer')); $header->addActionLink($raw); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setCollapsed(true); if ($messages) { $corpus->setInfoView( id(new PHUIInfoView()) ->setSeverity($severity) ->setErrors($messages)); } return $corpus; } private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef $ref) { $viewer = $this->getViewer(); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($ref->getFilePHID())) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file object for Git LFS ref "%s"!', $ref->getObjectHash())); } return $file; } private function buildBranchTable() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $branch = $drequest->getBranch(); $default_branch = $repository->getDefaultBranch(); if ($branch === $default_branch) { return null; } $pager = id(new PHUIPagerView()) ->setPageSize(10); try { $results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $branch, 'against' => $default_branch, 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, )); } catch (Exception $ex) { return null; } $history = DiffusionPathChange::newFromConduit($results['pathChanges']); $history = $pager->sliceResults($history); if (!$history) { return null; } $history_table = id(new DiffusionHistoryTableView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($history); $history_table->loadRevisions(); $history_table ->setParents($results['parents']) ->setFilterParents(true) ->setIsHead(true) ->setIsTail(!$pager->getHasMorePages()); $header = id(new PHUIHeaderView()) ->setHeader(pht('%s vs %s', $branch, $default_branch)); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($history_table); } } diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index 61c17e0877..eb46211d99 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -1,414 +1,414 @@ diffusionRequest) { throw new PhutilInvalidStateException('loadDiffusionContext'); } return $this->diffusionRequest; } protected function hasDiffusionRequest() { return (bool)$this->diffusionRequest; } public function willBeginExecution() { $request = $this->getRequest(); // Check if this is a VCS request, e.g. from "git clone", "hg clone", or // "svn checkout". If it is, we jump off into repository serving code to // process the request. $serve_controller = new DiffusionServeController(); if ($serve_controller->isVCSRequest($request)) { return $this->delegateToController($serve_controller); } return parent::willBeginExecution(); } protected function loadDiffusionContextForEdit() { return $this->loadContext( array( 'edit' => true, )); } protected function loadDiffusionContext() { return $this->loadContext(array()); } private function loadContext(array $options) { $request = $this->getRequest(); $viewer = $this->getViewer(); $identifier = $this->getRepositoryIdentifierFromRequest($request); $params = $options + array( 'repository' => $identifier, 'user' => $viewer, 'blob' => $this->getDiffusionBlobFromRequest($request), 'commit' => $request->getURIData('commit'), 'path' => $request->getURIData('path'), 'line' => $request->getURIData('line'), 'branch' => $request->getURIData('branch'), 'lint' => $request->getStr('lint'), ); $drequest = DiffusionRequest::newFromDictionary($params); if (!$drequest) { return new Aphront404Response(); } // If the client is making a request like "/diffusion/1/...", but the // repository has a different canonical path like "/diffusion/XYZ/...", // redirect them to the canonical path. $request_path = $request->getPath(); $repository = $drequest->getRepository(); $canonical_path = $repository->getCanonicalPath($request_path); if ($canonical_path !== null) { if ($canonical_path != $request_path) { return id(new AphrontRedirectResponse())->setURI($canonical_path); } } $this->diffusionRequest = $drequest; return null; } protected function getDiffusionBlobFromRequest(AphrontRequest $request) { return $request->getURIData('dblob'); } protected function getRepositoryIdentifierFromRequest( AphrontRequest $request) { $short_name = $request->getURIData('repositoryShortName'); if (strlen($short_name)) { // If the short name ends in ".git", ignore it. $short_name = preg_replace('/\\.git\z/', '', $short_name); return $short_name; } $identifier = $request->getURIData('repositoryCallsign'); if (strlen($identifier)) { return $identifier; } $id = $request->getURIData('repositoryID'); if (strlen($id)) { return (int)$id; } return null; } public function buildCrumbs(array $spec = array()) { $crumbs = $this->buildApplicationCrumbs(); $crumb_list = $this->buildCrumbList($spec); foreach ($crumb_list as $crumb) { $crumbs->addCrumb($crumb); } return $crumbs; } private function buildCrumbList(array $spec = array()) { $spec = $spec + array( 'commit' => null, 'tags' => null, 'branches' => null, 'view' => null, ); $crumb_list = array(); // On the home page, we don't have a DiffusionRequest. if ($this->hasDiffusionRequest()) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); } else { $drequest = null; $repository = null; } if (!$repository) { return $crumb_list; } $repository_name = $repository->getName(); if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $branch_name = $drequest->getBranch(); if ($branch_name) { $repository_name .= ' ('.$branch_name.')'; } } $crumb = id(new PHUICrumbView()) ->setName($repository_name); if (!$spec['view'] && !$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $crumb_list[] = $crumb; return $crumb_list; } $crumb->setHref( $drequest->generateURI( array( 'action' => 'branch', 'path' => '/', ))); $crumb_list[] = $crumb; $stable_commit = $drequest->getStableCommit(); $commit_name = $repository->formatCommitName($stable_commit, $local = true); $commit_uri = $repository->getCommitURI($stable_commit); if ($spec['tags']) { $crumb = new PHUICrumbView(); if ($spec['commit']) { $crumb->setName(pht('Tags for %s', $commit_name)); $crumb->setHref($commit_uri); } else { $crumb->setName(pht('Tags')); } $crumb_list[] = $crumb; return $crumb_list; } if ($spec['branches']) { $crumb = id(new PHUICrumbView()) ->setName(pht('Branches')); $crumb_list[] = $crumb; return $crumb_list; } if ($spec['commit']) { $crumb = id(new PHUICrumbView()) ->setName($commit_name); $crumb_list[] = $crumb; return $crumb_list; } $crumb = new PHUICrumbView(); $view = $spec['view']; switch ($view) { case 'history': $view_name = pht('History'); break; case 'browse': $view_name = pht('Browse'); break; case 'lint': $view_name = pht('Lint'); break; case 'change': $view_name = pht('Change'); break; case 'compare': $view_name = pht('Compare'); break; } $crumb = id(new PHUICrumbView()) ->setName($view_name); $crumb_list[] = $crumb; return $crumb_list; } protected function callConduitWithDiffusionRequest( $method, array $params = array()) { $user = $this->getRequest()->getUser(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params); } protected function callConduitMethod($method, array $params = array()) { $user = $this->getViewer(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params, true); } protected function getRepositoryControllerURI( PhabricatorRepository $repository, $path) { return $repository->getPathURI($path); } protected function renderPathLinks(DiffusionRequest $drequest, $action) { $path = $drequest->getPath(); $path_parts = array_filter(explode('/', trim($path, '/'))); $divider = phutil_tag( 'span', array( 'class' => 'phui-header-divider', ), '/'); $links = array(); if ($path_parts) { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => '', )), ), $drequest->getRepository()->getDisplayName()); $links[] = $divider; $accum = ''; $last_key = last_key($path_parts); foreach ($path_parts as $key => $part) { $accum .= '/'.$part; if ($key === $last_key) { $links[] = $part; } else { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => $accum.'/', )), ), $part); $links[] = $divider; } } } else { $links[] = $drequest->getRepository()->getDisplayName(); $links[] = $divider; } return $links; } protected function renderStatusMessage($title, $body) { return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle($title) ->setFlush(true) ->appendChild($body); } protected function renderTablePagerBox(PHUIPagerView $pager) { return id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) ->appendChild($pager); } protected function renderCommitHashTag(DiffusionRequest $drequest) { $stable_commit = $drequest->getStableCommit(); $commit = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit, true)); $tag = id(new PHUITagView()) ->setName($commit) ->setShade('indigo') ->setType(PHUITagView::TYPE_SHADE); return $tag; } protected function renderDirectoryReadme(DiffusionBrowseResultSet $browse) { $readme_path = $browse->getReadmePath(); if ($readme_path === null) { return null; } $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $repository = $drequest->getRepository(); $repository_phid = $repository->getPHID(); $stable_commit = $drequest->getStableCommit(); $stable_commit_hash = PhabricatorHash::digestForIndex($stable_commit); - $readme_path_hash = PhabricatorHash::digestForindex($readme_path); + $readme_path_hash = PhabricatorHash::digestForIndex($readme_path); $cache = PhabricatorCaches::getMutableStructureCache(); $cache_key = "diffusion". ".repository({$repository_phid})". ".commit({$stable_commit_hash})". ".readme({$readme_path_hash})"; $readme_cache = $cache->getKey($cache_key); if (!$readme_cache) { try { $result = $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', array( 'path' => $readme_path, 'commit' => $drequest->getStableCommit(), )); } catch (Exception $ex) { return null; } $file_phid = $result['filePHID']; if (!$file_phid) { return null; } $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { return null; } $corpus = $file->loadFileData(); $readme_cache = array( 'corpus' => $corpus, ); $cache->setKey($cache_key, $readme_cache); } $readme_corpus = $readme_cache['corpus']; if (!strlen($readme_corpus)) { return null; } return id(new DiffusionReadmeView()) ->setUser($this->getViewer()) ->setPath($readme_path) ->setContent($readme_corpus); } } diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php index eca8fe2a2f..c9df551c43 100644 --- a/src/applications/diffusion/controller/DiffusionHistoryController.php +++ b/src/applications/diffusion/controller/DiffusionHistoryController.php @@ -1,154 +1,154 @@ loadDiffusionContext(); if ($response) { return $response; } $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $pager = id(new PHUIPagerView()) ->readFromRequest($request); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => $pager->getOffset(), 'limit' => $pager->getPageSize() + 1, ); if (!$request->getBool('copies')) { $params['needDirectChanges'] = true; $params['needChildChanges'] = true; } $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', $params); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); $history = $pager->sliceResults($history); $show_graph = !strlen($drequest->getPath()); $history_table = id(new DiffusionHistoryTableView()) ->setUser($request->getUser()) ->setDiffusionRequest($drequest) ->setHistory($history); $history_table->loadRevisions(); if ($show_graph) { $history_table->setParents($history_results['parents']); $history_table->setIsHead(!$pager->getOffset()); $history_table->setIsTail(!$pager->getHasMorePages()); } $history_header = $this->buildHistoryHeader($drequest); $history_panel = id(new PHUIObjectBoxView()) ->setHeader($history_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($history_table); - $header = $this->buildHeader($drequest, $repository); + $header = $this->buildHeader($drequest); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'history', )); $crumbs->setBorder(true); $pager_box = $this->renderTablePagerBox($pager); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $history_panel, $pager_box, )); return $this->newPage() ->setTitle( array( pht('History'), $repository->getDisplayName(), )) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function buildHeader(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setPolicyObject($drequest->getRepository()) ->addTag($tag) ->setHeader($this->renderPathLinks($drequest, $mode = 'history')) ->setHeaderIcon('fa-clock-o'); return $header; } private function buildHistoryHeader(DiffusionRequest $drequest) { $viewer = $this->getViewer(); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $browse_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Browse')) ->setHref($browse_uri) ->setIcon('fa-files-o'); // TODO: Sometimes we do have a change view, we need to look at the most // recent history entry to figure it out. $request = $this->getRequest(); if ($request->getBool('copies')) { $branch_name = pht('Hide Copies/Branches'); $branch_uri = $request->getRequestURI() ->alter('offset', null) ->alter('copies', null); } else { $branch_name = pht('Show Copies/Branches'); $branch_uri = $request->getRequestURI() ->alter('offset', null) ->alter('copies', true); } $branch_button = id(new PHUIButtonView()) ->setTag('a') ->setText($branch_name) ->setIcon('fa-code-fork') ->setHref($branch_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('History')) ->addActionLink($browse_button) ->addActionLink($branch_button); return $header; } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php index 0fb220c385..324338443d 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryStatusManagementPanel.php @@ -1,506 +1,506 @@ getRepository(); // TODO: We could try to show a warning icon in more cases, but just // raise in the most serious cases for now. $messages = $this->loadStatusMessages($repository); $raw_error = $this->buildRepositoryRawError($repository, $messages); if ($raw_error) { return 'fa-exclamation-triangle red'; } return 'fa-check grey'; } protected function buildManagementPanelActions() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $update_uri = $repository->getPathURI('edit/update/'); return array( id(new PhabricatorActionView()) ->setIcon('fa-refresh') ->setName(pht('Update Now')) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref($update_uri), ); } public function buildManagementPanelContent() { $repository = $this->getRepository(); $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setViewer($viewer) ->setActionList($this->newActions()); $view->addProperty( pht('Update Frequency'), $this->buildRepositoryUpdateInterval($repository)); $messages = $this->loadStatusMessages($repository); $status = $this->buildRepositoryStatus($repository, $messages); $raw_error = $this->buildRepositoryRawError($repository, $messages); $view->addProperty(pht('Status'), $status); if ($raw_error) { $view->addSectionHeader(pht('Raw Error')); $view->addTextContent($raw_error); } return $this->newBox(pht('Status'), $view); } private function buildRepositoryUpdateInterval( PhabricatorRepository $repository) { $smart_wait = $repository->loadUpdateInterval(); $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Repository Updates'); return array( phutil_format_relative_time_detailed($smart_wait), " \xC2\xB7 ", phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Learn More')), ); } private function buildRepositoryStatus( PhabricatorRepository $repository, array $messages) { $viewer = $this->getViewer(); $is_cluster = $repository->getAlmanacServicePHID(); $view = new PHUIStatusListView(); if ($repository->isTracked()) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Repository Active'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey') ->setTarget(pht('Repository Inactive')) ->setNote( pht('Activate this repository to begin or resume import.'))); return $view; } $binaries = array(); $svnlook_check = false; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svn'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } if ($repository->isHosted()) { $proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS; $proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP; $can_http = $repository->canServeProtocol($proto_http, false) || $repository->canServeProtocol($proto_https, false); if ($can_http) { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git-http-backend'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svnserve'; $binaries[] = 'svnadmin'; $binaries[] = 'svnlook'; $svnlook_check = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } } $proto_ssh = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; $can_ssh = $repository->canServeProtocol($proto_ssh, false); if ($can_ssh) { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $binaries[] = 'git-receive-pack'; $binaries[] = 'git-upload-pack'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $binaries[] = 'svnserve'; $binaries[] = 'svnadmin'; $binaries[] = 'svnlook'; $svnlook_check = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $binaries[] = 'hg'; break; } } } $binaries = array_unique($binaries); if (!$is_cluster) { // We're only checking for binaries if we aren't running with a cluster // configuration. In theory, we could check for binaries on the // repository host machine, but we'd need to make this more complicated // to do that. foreach ($binaries as $binary) { $where = Filesystem::resolveBinary($binary); if (!$where) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(pht( "Unable to find this binary in the webserver's PATH. You may ". "need to configure %s.", $this->getEnvConfigLink()))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget( pht('Found Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(phutil_tag('tt', array(), $where))); } } // This gets checked generically above. However, for svn commit hooks, we // need this to be in environment.append-paths because subversion strips // PATH. if ($svnlook_check) { $where = Filesystem::resolveBinary('svnlook'); if ($where) { $path = substr($where, 0, strlen($where) - strlen('svnlook')); $dirs = PhabricatorEnv::getEnvConfig('environment.append-paths'); $in_path = false; foreach ($dirs as $dir) { if (Filesystem::isDescendant($path, $dir)) { $in_path = true; break; } } if (!$in_path) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) ->setNote(pht( 'Unable to find this binary in `%s`. '. 'You need to configure %s and include %s.', 'environment.append-paths', $this->getEnvConfigLink(), $path))); } } } } - $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd'); + $doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd'); $daemon_instructions = pht( 'Use %s to start daemons. See %s.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, ), pht('Managing Daemons with phd'))); $pull_daemon = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon')) ->setLimit(1) ->execute(); if ($pull_daemon) { // TODO: In a cluster environment, we need a daemon on this repository's // host, specifically, and we aren't checking for that right now. This // is a reasonable proxy for things being more-or-less correctly set up, // though. $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Pull Daemon Running'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Pull Daemon Not Running')) ->setNote($daemon_instructions)); } $task_daemon = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if ($task_daemon) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Task Daemon Running'))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Task Daemon Not Running')) ->setNote($daemon_instructions)); } if ($is_cluster) { // Just omit this status check for now in cluster environments. We // could make a service call and pull it from the repository host // eventually. } else if ($repository->usesLocalWorkingCopy()) { $local_parent = dirname($repository->getLocalPath()); if (Filesystem::pathExists($local_parent)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Storage Directory OK')) ->setNote(phutil_tag('tt', array(), $local_parent))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('No Storage Directory')) ->setNote( pht( 'Storage directory %s does not exist, or is not readable by '. 'the webserver. Create this directory or make it readable.', phutil_tag('tt', array(), $local_parent)))); return $view; } $local_path = $repository->getLocalPath(); $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Initialization Error')) ->setNote($message->getParameter('message'))); return $view; case PhabricatorRepositoryStatusMessage::CODE_OKAY: if (Filesystem::pathExists($local_path)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Working Copy OK')) ->setNote(phutil_tag('tt', array(), $local_path))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Working Copy Error')) ->setNote( pht( 'Working copy %s has been deleted, or is not '. 'readable by the webserver. Make this directory '. 'readable. If it has been deleted, the daemons should '. 'restore it automatically.', phutil_tag('tt', array(), $local_path)))); return $view; } break; default: $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green') ->setTarget(pht('Initializing Working Copy')) ->setNote(pht('Daemons are initializing the working copy.'))); return $view; } } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange') ->setTarget(pht('No Working Copy Yet')) ->setNote( pht('Waiting for daemons to build a working copy.'))); return $view; } } $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $message = $message->getParameter('message'); $suggestion = null; if (preg_match('/Permission denied \(publickey\)./', $message)) { $suggestion = pht( 'Public Key Error: This error usually indicates that the '. 'keypair you have configured does not have permission to '. 'access the repository.'); } $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget(pht('Update Error')) ->setNote($suggestion)); return $view; case PhabricatorRepositoryStatusMessage::CODE_OKAY: $ago = (PhabricatorTime::getNow() - $message->getEpoch()); $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Updates OK')) ->setNote( pht( 'Last updated %s (%s ago).', phabricator_datetime($message->getEpoch(), $viewer), phutil_format_relative_time_detailed($ago)))); break; } } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange') ->setTarget(pht('Waiting For Update')) ->setNote( pht('Waiting for daemons to read updates.'))); } if ($repository->isImporting()) { $ratio = $repository->loadImportProgress(); $percentage = sprintf('%.2f%%', 100 * $ratio); $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green') ->setTarget(pht('Importing')) ->setNote( pht('%s Complete', $percentage))); } else { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('Fully Imported'))); } if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) { $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_UP, 'indigo') ->setTarget(pht('Prioritized')) ->setNote(pht('This repository will be updated soon!'))); } return $view; } private function buildRepositoryRawError( PhabricatorRepository $repository, array $messages) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $raw_error = null; $message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH); if ($message) { switch ($message->getStatusCode()) { case PhabricatorRepositoryStatusMessage::CODE_ERROR: $raw_error = $message->getParameter('message'); break; } } if ($raw_error !== null) { if (!$can_edit) { $raw_message = pht( 'You must be able to edit a repository to see raw error messages '. 'because they sometimes disclose sensitive information.'); $raw_message = phutil_tag('em', array(), $raw_message); } else { $raw_message = phutil_escape_html_newlines($raw_error); } } else { $raw_message = null; } return $raw_message; } private function loadStatusMessages(PhabricatorRepository $repository) { $messages = id(new PhabricatorRepositoryStatusMessage()) ->loadAllWhere('repositoryID = %d', $repository->getID()); $messages = mpull($messages, null, 'getStatusType'); return $messages; } private function getEnvConfigLink() { $config_href = '/config/edit/environment.append-paths/'; return phutil_tag( 'a', array( 'href' => $config_href, ), 'environment.append-paths'); } } diff --git a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php index 41840bcb72..40bc63025d 100644 --- a/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php +++ b/src/applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php @@ -1,191 +1,191 @@ objects = $objects; - $phids = $query->getEvaluatedParameter('responsiblePHIDs', array()); + $phids = $query->getEvaluatedParameter('responsiblePHIDs'); if (!$phids) { throw new Exception( pht( 'You can not bucket results by required action without '. 'specifying "Responsible Users".')); } $phids = array_fuse($phids); $groups = array(); $groups[] = $this->newGroup() ->setName(pht('Needs Attention')) ->setNoDataString(pht('None of your commits have active concerns.')) ->setObjects($this->filterConcernRaised($phids)); $groups[] = $this->newGroup() ->setName(pht('Needs Verification')) ->setNoDataString(pht('No commits are awaiting your verification.')) ->setObjects($this->filterNeedsVerification($phids)); $groups[] = $this->newGroup() ->setName(pht('Ready to Audit')) ->setNoDataString(pht('No commits are waiting for you to audit them.')) ->setObjects($this->filterShouldAudit($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Authors')) ->setNoDataString(pht('None of your audits are waiting on authors.')) ->setObjects($this->filterWaitingOnAuthors($phids)); $groups[] = $this->newGroup() ->setName(pht('Waiting on Auditors')) ->setNoDataString(pht('None of your commits are waiting on audit.')) ->setObjects($this->filterWaitingOnAuditors($phids)); // Because you can apply these buckets to queries which include revisions // that have been closed, add an "Other" bucket if we still have stuff // that didn't get filtered into any of the previous buckets. if ($this->objects) { $groups[] = $this->newGroup() ->setName(pht('Other Commits')) ->setObjects($this->objects); } return $groups; } private function filterConcernRaised(array $phids) { $results = array(); $objects = $this->objects; $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; foreach ($objects as $key => $object) { if (empty($phids[$object->getAuthorPHID()])) { continue; } if ($object->getAuditStatus() != $status_concern) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterNeedsVerification(array $phids) { $results = array(); $objects = $this->objects; $status_verify = PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION; $has_concern = array( PhabricatorAuditStatusConstants::CONCERNED, ); $has_concern = array_fuse($has_concern); foreach ($objects as $key => $object) { if (isset($phids[$object->getAuthorPHID()])) { continue; } if ($object->getAuditStatus() != $status_verify) { continue; } if (!$this->hasAuditorsWithStatus($object, $phids, $has_concern)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterShouldAudit(array $phids) { $results = array(); $objects = $this->objects; $should_audit = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::AUDIT_REQUESTED, ); $should_audit = array_fuse($should_audit); foreach ($objects as $key => $object) { if (isset($phids[$object->getAuthorPHID()])) { continue; } if (!$this->hasAuditorsWithStatus($object, $phids, $should_audit)) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingOnAuthors(array $phids) { $results = array(); $objects = $this->objects; $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; foreach ($objects as $key => $object) { if ($object->getAuditStatus() != $status_concern) { continue; } if (isset($phids[$object->getAuthorPHID()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } private function filterWaitingOnAuditors(array $phids) { $results = array(); $objects = $this->objects; $status_waiting = array( PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT, PhabricatorAuditCommitStatusConstants::NEEDS_VERIFICATION, PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED, ); $status_waiting = array_fuse($status_waiting); foreach ($objects as $key => $object) { if (empty($status_waiting[$object->getAuditStatus()])) { continue; } $results[$key] = $object; unset($this->objects[$key]); } return $results; } } diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php index 520a51e26b..f7f8d2c4ac 100644 --- a/src/applications/diviner/atom/DivinerAtom.php +++ b/src/applications/diviner/atom/DivinerAtom.php @@ -1,436 +1,436 @@ getBook(), $this->getType(), $this->getContext(), $this->getName(), $this->getFile(), - sprintf('%08', $this->getLine()), + sprintf('%08d', $this->getLine()), )); } public function setBook($book) { $this->book = $book; return $this; } public function getBook() { return $this->book; } public function setContext($context) { $this->context = $context; return $this; } public function getContext() { return $this->context; } public static function getAtomSerializationVersion() { return 2; } public function addWarning($warning) { $this->warnings[] = $warning; return $this; } public function getWarnings() { return $this->warnings; } public function setDocblockRaw($docblock_raw) { $this->docblockRaw = $docblock_raw; $parser = new PhutilDocblockParser(); list($text, $meta) = $parser->parse($docblock_raw); $this->docblockText = $text; $this->docblockMeta = $meta; return $this; } public function getDocblockRaw() { return $this->docblockRaw; } public function getDocblockText() { if ($this->docblockText === null) { throw new PhutilInvalidStateException('setDocblockRaw'); } return $this->docblockText; } public function getDocblockMeta() { if ($this->docblockMeta === null) { throw new PhutilInvalidStateException('setDocblockRaw'); } return $this->docblockMeta; } public function getDocblockMetaValue($key, $default = null) { $meta = $this->getDocblockMeta(); return idx($meta, $key, $default); } public function setDocblockMetaValue($key, $value) { $meta = $this->getDocblockMeta(); $meta[$key] = $value; $this->docblockMeta = $meta; return $this; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setFile($file) { $this->file = $file; return $this; } public function getFile() { return $this->file; } public function setLine($line) { $this->line = $line; return $this; } public function getLine() { return $this->line; } public function setContentRaw($content_raw) { $this->contentRaw = $content_raw; return $this; } public function getContentRaw() { return $this->contentRaw; } public function setHash($hash) { $this->hash = $hash; return $this; } public function addLink(DivinerAtomRef $ref) { $this->links[] = $ref; return $this; } public function addExtends(DivinerAtomRef $ref) { $this->extends[] = $ref; return $this; } public function getLinkDictionaries() { return mpull($this->links, 'toDictionary'); } public function getExtendsDictionaries() { return mpull($this->extends, 'toDictionary'); } public function getExtends() { return $this->extends; } public function getHash() { if ($this->hash) { return $this->hash; } $parts = array( $this->getBook(), $this->getType(), $this->getName(), $this->getFile(), $this->getLine(), $this->getLength(), $this->getLanguage(), $this->getContentRaw(), $this->getDocblockRaw(), $this->getProperties(), $this->getChildHashes(), mpull($this->extends, 'toHash'), mpull($this->links, 'toHash'), ); $this->hash = md5(serialize($parts)).'N'; return $this->hash; } public function setLength($length) { $this->length = $length; return $this; } public function getLength() { return $this->length; } public function setLanguage($language) { $this->language = $language; return $this; } public function getLanguage() { return $this->language; } public function addChildHash($child_hash) { $this->childHashes[] = $child_hash; return $this; } public function getChildHashes() { if (!$this->childHashes && $this->children) { $this->childHashes = mpull($this->children, 'getHash'); } return $this->childHashes; } public function setParentHash($parent_hash) { if ($this->parentHash) { throw new Exception(pht('Atom already has a parent!')); } $this->parentHash = $parent_hash; return $this; } public function hasParent() { return $this->parent || $this->parentHash; } public function setParent(DivinerAtom $atom) { if ($this->parentHash) { throw new Exception(pht('Parent hash has already been computed!')); } $this->parent = $atom; return $this; } public function getParentHash() { if ($this->parent && !$this->parentHash) { $this->parentHash = $this->parent->getHash(); } return $this->parentHash; } public function addChild(DivinerAtom $atom) { if ($this->childHashes) { throw new Exception(pht('Child hashes have already been computed!')); } $atom->setParent($this); $this->children[] = $atom; return $this; } public function getURI() { $parts = array(); $parts[] = phutil_escape_uri_path_component($this->getType()); if ($this->getContext()) { $parts[] = phutil_escape_uri_path_component($this->getContext()); } $parts[] = phutil_escape_uri_path_component($this->getName()); $parts[] = null; return implode('/', $parts); } public function toDictionary() { // NOTE: If you change this format, bump the format version in // @{method:getAtomSerializationVersion}. return array( 'book' => $this->getBook(), 'type' => $this->getType(), 'name' => $this->getName(), 'file' => $this->getFile(), 'line' => $this->getLine(), 'hash' => $this->getHash(), 'uri' => $this->getURI(), 'length' => $this->getLength(), 'context' => $this->getContext(), 'language' => $this->getLanguage(), 'docblockRaw' => $this->getDocblockRaw(), 'warnings' => $this->getWarnings(), 'parentHash' => $this->getParentHash(), 'childHashes' => $this->getChildHashes(), 'extends' => $this->getExtendsDictionaries(), 'links' => $this->getLinkDictionaries(), 'ref' => $this->getRef()->toDictionary(), 'properties' => $this->getProperties(), ); } public function getRef() { $title = null; if ($this->docblockMeta) { $title = $this->getDocblockMetaValue('title'); } return id(new DivinerAtomRef()) ->setBook($this->getBook()) ->setContext($this->getContext()) ->setType($this->getType()) ->setName($this->getName()) ->setTitle($title) ->setGroup($this->getProperty('group')); } public static function newFromDictionary(array $dictionary) { $atom = id(new DivinerAtom()) ->setBook(idx($dictionary, 'book')) ->setType(idx($dictionary, 'type')) ->setName(idx($dictionary, 'name')) ->setFile(idx($dictionary, 'file')) ->setLine(idx($dictionary, 'line')) ->setHash(idx($dictionary, 'hash')) ->setLength(idx($dictionary, 'length')) ->setContext(idx($dictionary, 'context')) ->setLanguage(idx($dictionary, 'language')) ->setParentHash(idx($dictionary, 'parentHash')) ->setDocblockRaw(idx($dictionary, 'docblockRaw')) ->setProperties(idx($dictionary, 'properties')); foreach (idx($dictionary, 'warnings', array()) as $warning) { $atom->addWarning($warning); } foreach (idx($dictionary, 'childHashes', array()) as $child) { $atom->addChildHash($child); } foreach (idx($dictionary, 'extends', array()) as $extends) { $atom->addExtends(DivinerAtomRef::newFromDictionary($extends)); } return $atom; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperties() { return $this->properties; } public function setProperties(array $properties) { $this->properties = $properties; return $this; } public static function getThisAtomIsNotDocumentedString($type) { switch ($type) { case self::TYPE_ARTICLE: return pht('This article is not documented.'); case self::TYPE_CLASS: return pht('This class is not documented.'); case self::TYPE_FILE: return pht('This file is not documented.'); case self::TYPE_FUNCTION: return pht('This function is not documented.'); case self::TYPE_INTERFACE: return pht('This interface is not documented.'); case self::TYPE_METHOD: return pht('This method is not documented.'); default: phlog(pht("Need translation for '%s'.", $type)); return pht('This %s is not documented.', $type); } } public static function getAllTypes() { return array( self::TYPE_ARTICLE, self::TYPE_CLASS, self::TYPE_FILE, self::TYPE_FUNCTION, self::TYPE_INTERFACE, self::TYPE_METHOD, ); } public static function getAtomTypeNameString($type) { switch ($type) { case self::TYPE_ARTICLE: return pht('Article'); case self::TYPE_CLASS: return pht('Class'); case self::TYPE_FILE: return pht('File'); case self::TYPE_FUNCTION: return pht('Function'); case self::TYPE_INTERFACE: return pht('Interface'); case self::TYPE_METHOD: return pht('Method'); default: phlog(pht("Need translation for '%s'.", $type)); return ucwords($type); } } } diff --git a/src/applications/diviner/renderer/DivinerDefaultRenderer.php b/src/applications/diviner/renderer/DivinerDefaultRenderer.php index 6ec70f2a3c..a4c551e93d 100644 --- a/src/applications/diviner/renderer/DivinerDefaultRenderer.php +++ b/src/applications/diviner/renderer/DivinerDefaultRenderer.php @@ -1,263 +1,263 @@ renderAtomTitle($atom), $this->renderAtomProperties($atom), $this->renderAtomDescription($atom), ); return phutil_tag( 'div', array( 'class' => 'diviner-atom', ), $out); } protected function renderAtomTitle(DivinerAtom $atom) { $name = $this->renderAtomName($atom); $type = $this->renderAtomType($atom); return phutil_tag( 'h1', array( 'class' => 'atom-title', ), array($name, ' ', $type)); } protected function renderAtomName(DivinerAtom $atom) { return phutil_tag( 'div', array( 'class' => 'atom-name', ), $this->getAtomName($atom)); } protected function getAtomName(DivinerAtom $atom) { if ($atom->getDocblockMetaValue('title')) { return $atom->getDocblockMetaValue('title'); } return $atom->getName(); } protected function renderAtomType(DivinerAtom $atom) { return phutil_tag( 'div', array( 'class' => 'atom-name', ), $this->getAtomType($atom)); } protected function getAtomType(DivinerAtom $atom) { return ucwords($atom->getType()); } protected function renderAtomProperties(DivinerAtom $atom) { $props = $this->getAtomProperties($atom); $out = array(); foreach ($props as $prop) { list($key, $value) = $prop; $out[] = phutil_tag('dt', array(), $key); $out[] = phutil_tag('dd', array(), $value); } return phutil_tag( 'dl', array( 'class' => 'atom-properties', ), $out); } protected function getAtomProperties(DivinerAtom $atom) { $properties = array(); $properties[] = array( pht('Defined'), $atom->getFile().':'.$atom->getLine(), ); return $properties; } protected function renderAtomDescription(DivinerAtom $atom) { $text = $this->getAtomDescription($atom); $engine = $this->getBlockMarkupEngine(); $this->pushAtomStack($atom); $description = $engine->markupText($text); - $this->popAtomStack($atom); + $this->popAtomStack(); return phutil_tag( 'div', array( 'class' => 'atom-description', ), $description); } protected function getAtomDescription(DivinerAtom $atom) { return $atom->getDocblockText(); } public function renderAtomSummary(DivinerAtom $atom) { $text = $this->getAtomSummary($atom); $engine = $this->getInlineMarkupEngine(); $this->pushAtomStack($atom); $summary = $engine->markupText($text); $this->popAtomStack(); return phutil_tag( 'span', array( 'class' => 'atom-summary', ), $summary); } public function getAtomSummary(DivinerAtom $atom) { if ($atom->getDocblockMetaValue('summary')) { return $atom->getDocblockMetaValue('summary'); } $text = $this->getAtomDescription($atom); return PhabricatorMarkupEngine::summarize($text); } public function renderAtomIndex(array $refs) { $refs = msort($refs, 'getSortKey'); $groups = mgroup($refs, 'getGroup'); $out = array(); foreach ($groups as $group_key => $refs) { $out[] = phutil_tag( 'h1', array( 'class' => 'atom-group-name', ), $this->getGroupName($group_key)); $items = array(); foreach ($refs as $ref) { $items[] = phutil_tag( 'li', array( 'class' => 'atom-index-item', ), array( $this->renderAtomRefLink($ref), ' - ', $ref->getSummary(), )); } $out[] = phutil_tag( 'ul', array( 'class' => 'atom-index-list', ), $items); } return phutil_tag( 'div', array( 'class' => 'atom-index', ), $out); } protected function getGroupName($group_key) { return $group_key; } protected function getBlockMarkupEngine() { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); $engine->setConfig('viewer', new PhabricatorUser()); $engine->setConfig('diviner.renderer', $this); $engine->setConfig('header.generate-toc', true); return $engine; } protected function getInlineMarkupEngine() { return $this->getBlockMarkupEngine(); } public function normalizeAtomRef(DivinerAtomRef $ref) { if (!strlen($ref->getBook())) { $ref->setBook($this->getConfig('name')); } if ($ref->getBook() != $this->getConfig('name')) { // If the ref is from a different book, we can't normalize it. // Just return it as-is if it has enough information to resolve. if ($ref->getName() && $ref->getType()) { return $ref; } else { return null; } } $atom = $this->getPublisher()->findAtomByRef($ref); if ($atom) { return $atom->getRef(); } return null; } protected function getAtomHrefDepth(DivinerAtom $atom) { if ($atom->getContext()) { return 4; } else { return 3; } } public function getHrefForAtomRef(DivinerAtomRef $ref) { $depth = 1; $atom = $this->peekAtomStack(); if ($atom) { $depth = $this->getAtomHrefDepth($atom); } $href = str_repeat('../', $depth); $book = $ref->getBook(); $type = $ref->getType(); $name = $ref->getName(); $context = $ref->getContext(); $href .= $book.'/'.$type.'/'; if ($context !== null) { $href .= $context.'/'; } $href .= $name.'/index.html'; return $href; } protected function renderAtomRefLink(DivinerAtomRef $ref) { return phutil_tag( 'a', array( 'href' => $this->getHrefForAtomRef($ref), ), $ref->getTitle()); } } diff --git a/src/applications/diviner/workflow/DivinerWorkflow.php b/src/applications/diviner/workflow/DivinerWorkflow.php index 152910a477..69fcc67f48 100644 --- a/src/applications/diviner/workflow/DivinerWorkflow.php +++ b/src/applications/diviner/workflow/DivinerWorkflow.php @@ -1,76 +1,76 @@ bookConfigPath; } protected function getConfig($key, $default = null) { return idx($this->config, $key, $default); } protected function getAllConfig() { return $this->config; } protected function readBookConfiguration($book_path) { if ($book_path === null) { throw new PhutilArgumentUsageException( pht( 'Specify a Diviner book configuration file with %s.', '--book')); } $book_data = Filesystem::readFile($book_path); $book = phutil_json_decode($book_data); PhutilTypeSpec::checkMap( $book, array( 'name' => 'string', 'title' => 'optional string', 'short' => 'optional string', 'preface' => 'optional string', 'root' => 'optional string', 'uri.source' => 'optional string', 'rules' => 'optional map', 'exclude' => 'optional regex|list', 'groups' => 'optional map>', )); // If the book specifies a "root", resolve it; otherwise, use the directory // the book configuration file lives in. $full_path = dirname(Filesystem::resolvePath($book_path)); if (empty($book['root'])) { $book['root'] = '.'; } $book['root'] = Filesystem::resolvePath($book['root'], $full_path); if (!preg_match('/^[a-z][a-z-]*\z/', $book['name'])) { $name = $book['name']; throw new PhutilArgumentUsageException( pht( "Book configuration '%s' has name '%s', but book names must ". "include only lowercase letters and hyphens.", $book_path, $name)); } foreach (idx($book, 'groups', array()) as $group) { - PhutilTypeSpec::checkmap( + PhutilTypeSpec::checkMap( $group, array( 'name' => 'string', 'include' => 'optional regex|list', )); } $this->bookConfigPath = $book_path; $this->config = $book; } } diff --git a/src/applications/herald/controller/HeraldController.php b/src/applications/herald/controller/HeraldController.php index 355d3b90f9..1a5d2084dd 100644 --- a/src/applications/herald/controller/HeraldController.php +++ b/src/applications/herald/controller/HeraldController.php @@ -1,40 +1,40 @@ buildSideNavView(true)->getMenu(); + return $this->buildSideNavView()->getMenu(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Herald Rule')) ->setHref($this->getApplicationURI('create/')) ->setIcon('fa-plus-square')); return $crumbs; } public function buildSideNavView() { $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new HeraldRuleSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->addLabel(pht('Utilities')) ->addFilter('test', pht('Test Console')) ->addFilter('transcript', pht('Transcripts')); $nav->selectFilter(null); return $nav; } } diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php index e2c5f6928b..88df18db24 100644 --- a/src/applications/herald/query/HeraldRuleQuery.php +++ b/src/applications/herald/query/HeraldRuleQuery.php @@ -1,289 +1,284 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withRuleTypes(array $types) { $this->ruleTypes = $types; return $this; } public function withContentTypes(array $types) { $this->contentTypes = $types; return $this; } - public function withExecutableRules($executable) { - $this->executable = $executable; - return $this; - } - public function withDisabled($disabled) { $this->disabled = $disabled; return $this; } public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } public function withTriggerObjectPHIDs(array $phids) { $this->triggerObjectPHIDs = $phids; return $this; } public function needConditionsAndActions($need) { $this->needConditionsAndActions = $need; return $this; } public function needAppliedToPHIDs(array $phids) { $this->needAppliedToPHIDs = $phids; return $this; } public function needValidateAuthors($need) { $this->needValidateAuthors = $need; return $this; } protected function loadPage() { $table = new HeraldRule(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT rule.* FROM %T rule %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $rules) { $rule_ids = mpull($rules, 'getID'); // Filter out any rules that have invalid adapters, or have adapters the // viewer isn't permitted to see or use (for example, Differential rules // if the user can't use Differential or Differential is not installed). $types = HeraldAdapter::getEnabledAdapterMap($this->getViewer()); foreach ($rules as $key => $rule) { if (empty($types[$rule->getContentType()])) { $this->didRejectResult($rule); unset($rules[$key]); } } if ($this->needValidateAuthors) { $this->validateRuleAuthors($rules); } if ($this->needConditionsAndActions) { $conditions = id(new HeraldCondition())->loadAllWhere( 'ruleID IN (%Ld)', $rule_ids); $conditions = mgroup($conditions, 'getRuleID'); $actions = id(new HeraldActionRecord())->loadAllWhere( 'ruleID IN (%Ld)', $rule_ids); $actions = mgroup($actions, 'getRuleID'); foreach ($rules as $rule) { $rule->attachActions(idx($actions, $rule->getID(), array())); $rule->attachConditions(idx($conditions, $rule->getID(), array())); } } if ($this->needAppliedToPHIDs) { $conn_r = id(new HeraldRule())->establishConnection('r'); $applied = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE ruleID IN (%Ld) AND phid IN (%Ls)', HeraldRule::TABLE_RULE_APPLIED, $rule_ids, $this->needAppliedToPHIDs); $map = array(); foreach ($applied as $row) { $map[$row['ruleID']][$row['phid']] = true; } foreach ($rules as $rule) { foreach ($this->needAppliedToPHIDs as $phid) { $rule->setRuleApplied( $phid, isset($map[$rule->getID()][$phid])); } } } $object_phids = array(); foreach ($rules as $rule) { if ($rule->isObjectRule()) { $object_phids[] = $rule->getTriggerObjectPHID(); } } if ($object_phids) { $objects = id(new PhabricatorObjectQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); } else { $objects = array(); } foreach ($rules as $key => $rule) { if ($rule->isObjectRule()) { $object = idx($objects, $rule->getTriggerObjectPHID()); if (!$object) { unset($rules[$key]); continue; } $rule->attachTriggerObject($object); } } return $rules; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'rule.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'rule.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs) { $where[] = qsprintf( $conn_r, 'rule.authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->ruleTypes) { $where[] = qsprintf( $conn_r, 'rule.ruleType IN (%Ls)', $this->ruleTypes); } if ($this->contentTypes) { $where[] = qsprintf( $conn_r, 'rule.contentType IN (%Ls)', $this->contentTypes); } if ($this->disabled !== null) { $where[] = qsprintf( $conn_r, 'rule.isDisabled = %d', (int)$this->disabled); } if ($this->datasourceQuery) { $where[] = qsprintf( $conn_r, 'rule.name LIKE %>', $this->datasourceQuery); } if ($this->triggerObjectPHIDs) { $where[] = qsprintf( $conn_r, 'rule.triggerObjectPHID IN (%Ls)', $this->triggerObjectPHIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function validateRuleAuthors(array $rules) { // "Global" and "Object" rules always have valid authors. foreach ($rules as $key => $rule) { if ($rule->isGlobalRule() || $rule->isObjectRule()) { $rule->attachValidAuthor(true); unset($rules[$key]); continue; } } if (!$rules) { return; } // For personal rules, the author needs to exist and not be disabled. $user_phids = mpull($rules, 'getAuthorPHID'); $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($user_phids) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($rules as $key => $rule) { $author_phid = $rule->getAuthorPHID(); if (empty($users[$author_phid])) { $rule->attachValidAuthor(false); continue; } if (!$users[$author_phid]->isUserActivated()) { $rule->attachValidAuthor(false); continue; } $rule->attachValidAuthor(true); $rule->attachAuthor($users[$author_phid]); } } public function getQueryApplicationClass() { return 'PhabricatorHeraldApplication'; } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php index 3804885edb..8f84f60339 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php @@ -1,272 +1,272 @@ getViewer(); $id = $request->getURIData('id'); if (!$id) { $is_create = true; $this->requireApplicationCapability( LegalpadCreateDocumentsCapability::CAPABILITY); $document = LegalpadDocument::initializeNewDocument($viewer); $body = id(new LegalpadDocumentBody()) ->setCreatorPHID($viewer->getPHID()); $document->attachDocumentBody($body); $document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID); } else { $is_create = false; $document = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->needDocumentBodies(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($id)) ->executeOne(); if (!$document) { return new Aphront404Response(); } } $e_title = true; $e_text = true; $title = $document->getDocumentBody()->getTitle(); $text = $document->getDocumentBody()->getText(); $v_signature_type = $document->getSignatureType(); $v_preamble = $document->getPreamble(); $v_require_signature = $document->getRequireSignature(); $errors = array(); $can_view = null; $can_edit = null; if ($request->isFormPost()) { $xactions = array(); $title = $request->getStr('title'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('The document title may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_TITLE) ->setNewValue($title); } $text = $request->getStr('text'); if (!strlen($text)) { $e_text = pht('Required'); $errors[] = pht('The document may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_TEXT) ->setNewValue($text); } $can_view = $request->getStr('can_view'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($can_view); $can_edit = $request->getStr('can_edit'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($can_edit); if ($is_create) { $v_signature_type = $request->getStr('signatureType'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_SIGNATURE_TYPE) ->setNewValue($v_signature_type); } $v_preamble = $request->getStr('preamble'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_PREAMBLE) ->setNewValue($v_preamble); $v_require_signature = $request->getBool('requireSignature', 0); if ($v_require_signature) { if (!$viewer->getIsAdmin()) { $errors[] = pht('Only admins may require signature.'); } $individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; if ($v_signature_type != $individual) { $errors[] = pht( 'Only documents with signature type "individual" may require '. 'signing to use Phabricator.'); } } if ($viewer->getIsAdmin()) { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransaction::TYPE_REQUIRE_SIGNATURE) ->setNewValue($v_require_signature); } if (!$errors) { $editor = id(new LegalpadDocumentEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($viewer); $xactions = $editor->applyTransactions($document, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('view/'.$document->getID())); } } if ($errors) { // set these to what was specified in the form on post $document->setViewPolicy($can_view); $document->setEditPolicy($can_edit); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setID('document-title') ->setLabel(pht('Title')) ->setError($e_title) ->setValue($title) ->setName('title')); if ($is_create) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Who Should Sign?')) ->setName(pht('signatureType')) ->setValue($v_signature_type) ->setOptions(LegalpadDocument::getSignatureTypeMap())); $show_require = true; $caption = pht('Applies only to documents individuals sign.'); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Who Should Sign?')) ->setValue($document->getSignatureTypeName())); $individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; $show_require = $document->getSignatureType() == $individual; $caption = null; } if ($show_require) { $form ->appendChild( id(new AphrontFormCheckboxControl()) ->setDisabled(!$viewer->getIsAdmin()) ->setLabel(pht('Require Signature')) ->addCheckbox( 'requireSignature', 'requireSignature', pht('Should signing this document be required to use Phabricator?'), $v_require_signature) ->setCaption($caption)); } $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setID('preamble') ->setLabel(pht('Preamble')) ->setValue($v_preamble) ->setName('preamble') ->setCaption( pht('Optional help text for users signing this document.'))) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setID('document-text') ->setLabel(pht('Document Body')) ->setError($e_text) ->setValue($text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('text')); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($document) ->execute(); $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_edit')); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); + $crumbs = $this->buildApplicationCrumbs(); $submit = new AphrontFormSubmitControl(); if ($is_create) { $submit->setValue(pht('Create Document')); $submit->addCancelButton($this->getApplicationURI()); $title = pht('Create Document'); $short = pht('Create'); $header_icon = 'fa-plus-square'; } else { $submit->setValue(pht('Save Document')); $submit->addCancelButton( $this->getApplicationURI('view/'.$document->getID())); $title = pht('Edit Document: %s', $document->getTitle()); $short = pht('Edit'); $header_icon = 'fa-pencil'; $crumbs->addTextCrumb( $document->getMonogram(), $this->getApplicationURI('view/'.$document->getID())); } $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Document')) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $crumbs->addTextCrumb($short); $crumbs->setBorder(true); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader($document->getTitle()) ->setPreviewURI($this->getApplicationURI('document/preview/')) ->setControlID('document-text') ->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon($header_icon); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $form_box, $preview, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentManageController.php b/src/applications/legalpad/controller/LegalpadDocumentManageController.php index 1a39f2c143..6d9360a731 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentManageController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentManageController.php @@ -1,211 +1,211 @@ getViewer(); $id = $request->getURIData('id'); // NOTE: We require CAN_EDIT to view this page. $document = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needDocumentBodies(true) ->needContributors(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $document_body = $document->getDocumentBody(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $engine->addObject( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $timeline = $this->buildTransactionTimeline( $document, new LegalpadTransactionQuery(), $engine); $title = $document_body->getTitle(); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($document) ->setHeaderIcon('fa-gavel'); $curtain = $this->buildCurtainView($document); $properties = $this->buildPropertyView($document, $engine); $document_view = $this->buildDocumentView($document, $engine); $comment_form_id = celerity_generate_unique_node_id(); $add_comment = $this->buildAddCommentView($document, $comment_form_id); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $document->getMonogram(), '/'.$document->getMonogram()); $crumbs->addTextCrumb(pht('Manage')); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $properties, $document_view, $timeline, $add_comment, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($document->getPHID())) ->appendChild($view); } private function buildDocumentView( LegalpadDocument $document, PhabricatorMarkupEngine $engine) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $document_body = $document->getDocumentBody(); $document_text = $engine->getOutput( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $preamble_box = null; if (strlen($document->getPreamble())) { $preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble()); $view->addTextContent($preamble_text); $view->addSectionHeader(''); $view->addTextContent($document_text); } else { $view->addTextContent($document_text); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('DOCUMENT')) ->addPropertyList($view) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); } private function buildCurtainView(LegalpadDocument $document) { $viewer = $this->getViewer(); $curtain = $this->newCurtainView($document); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $document, PhabricatorPolicyCapability::CAN_EDIT); $doc_id = $document->getID(); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil-square') ->setName(pht('View/Sign Document')) ->setHref('/'.$document->getMonogram())); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Document')) ->setHref($this->getApplicationURI('/edit/'.$doc_id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-terminal') ->setName(pht('View Signatures')) ->setHref($this->getApplicationURI('/signatures/'.$doc_id.'/'))); return $curtain; } private function buildPropertyView( LegalpadDocument $document, PhabricatorMarkupEngine $engine) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $properties->addProperty( pht('Signature Type'), $document->getSignatureTypeName()); $properties->addProperty( pht('Last Updated'), phabricator_datetime($document->getDateModified(), $viewer)); $properties->addProperty( pht('Updated By'), $viewer->renderHandle($document->getDocumentBody()->getCreatorPHID())); $properties->addProperty( pht('Versions'), $document->getVersions()); if ($document->getContributors()) { $properties->addProperty( pht('Contributors'), $viewer ->renderHandleList($document->getContributors()) ->setAsInline(true)); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->addPropertyList($properties) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); } private function buildAddCommentView( LegalpadDocument $document, $comment_form_id) { $viewer = $this->getViewer(); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $document->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('Debate Legislation'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($document->getPHID()) ->setFormID($comment_form_id) ->setHeaderText($title) ->setDraft($draft) ->setSubmitButtonName(pht('Add Comment')) ->setAction($this->getApplicationURI('/comment/'.$document->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/legalpad/storage/LegalpadDocumentBody.php b/src/applications/legalpad/storage/LegalpadDocumentBody.php index a55583428c..a3fdf20f5f 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentBody.php +++ b/src/applications/legalpad/storage/LegalpadDocumentBody.php @@ -1,80 +1,77 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'version' => 'uint32', 'title' => 'text255', 'text' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_document' => array( 'columns' => array('documentPHID', 'version'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_LEGB); } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return 'LEGB:'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_TEXT: $text = $this->getText(); break; - case self::MARKUP_FIELD_TITLE: - $text = $this->getTitle(); - break; default: throw new Exception(pht('Unknown field: %s', $field)); break; } return $text; } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } } diff --git a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php index b44895ae87..2617c755df 100644 --- a/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php +++ b/src/applications/lipsum/generator/PhabricatorTestDataGenerator.php @@ -1,116 +1,118 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } protected function loadRandomPHID($table) { $conn_r = $table->establishConnection('r'); $row = queryfx_one( $conn_r, 'SELECT phid FROM %T ORDER BY RAND() LIMIT 1', $table->getTableName()); if (!$row) { return null; } return $row['phid']; } protected function loadRandomUser() { $viewer = $this->getViewer(); $user_phid = $this->loadRandomPHID(new PhabricatorUser()); $user = null; if ($user_phid) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($user_phid)) ->executeOne(); } if (!$user) { throw new Exception( pht( 'Failed to load a random user. You may need to generate more '. 'test users first.')); } return $user; } protected function getLipsumContentSource() { return PhabricatorContentSource::newForSource( PhabricatorLipsumContentSource::SOURCECONST); } /** * Roll `n` dice with `d` sides each, then add `bonus` and return the sum. */ protected function roll($n, $d, $bonus = 0) { $sum = 0; for ($ii = 0; $ii < $n; $ii++) { $sum += mt_rand(1, $d); } $sum += $bonus; return $sum; } protected function newEmptyTransaction() { throw new PhutilMethodNotImplementedException(); } protected function newTransaction($type, $value, $metadata = array()) { $xaction = $this->newEmptyTransaction() ->setTransactionType($type) ->setNewValue($value); foreach ($metadata as $key => $value) { $xaction->setMetadataValue($key, $value); } return $xaction; } public function loadOneRandom($classname) { try { return newv($classname, array()) ->loadOneWhere('1 = 1 ORDER BY RAND() LIMIT 1'); } catch (PhutilMissingSymbolException $ex) { throw new PhutilMissingSymbolException( + $classname, + pht('class'), pht( - 'Unable to load symbol %s: this class does not exit.', + 'Unable to load symbol %s: this class does not exist.', $classname)); } } public function loadPhabrictorUserPHID() { return $this->loadOneRandom('PhabricatorUser')->getPHID(); } public function loadPhabrictorUser() { return $this->loadOneRandom('PhabricatorUser'); } } diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index 17ba6cb548..c80a1a462a 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -1,64 +1,64 @@ buildSideNavView(true)->getMenu(); + return $this->buildSideNavView()->getMenu(); } public function buildSideNavView() { $viewer = $this->getViewer(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); if ($viewer->isLoggedIn()) { // For now, don't give logged-out users access to reports. $nav->addLabel(pht('Reports')); $nav->addFilter('report', pht('Reports')); } $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); id(new ManiphestEditEngine()) ->setViewer($this->getViewer()) ->addActionToCrumbs($crumbs); return $crumbs; } public function renderSingleTask(ManiphestTask $task) { $request = $this->getRequest(); $user = $request->getUser(); $phids = $task->getProjectPHIDs(); if ($task->getOwnerPHID()) { $phids[] = $task->getOwnerPHID(); } $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($phids) ->execute(); $view = id(new ManiphestTaskListView()) ->setUser($user) ->setShowSubpriorityControls(!$request->getStr('ungrippable')) ->setShowBatchControls(true) ->setHandles($handles) ->setTasks(array($task)); return $view; } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index ca0fe5466e..369986705c 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,558 +1,558 @@ getViewer(); $id = $request->getURIData('id'); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($task); $edit_engine = id(new ManiphestEditEngine()) ->setViewer($viewer) ->setTargetObject($task); $edge_types = array( ManiphestTaskHasCommitEdgeType::EDGECONST, ManiphestTaskHasRevisionEdgeType::EDGECONST, ManiphestTaskHasMockEdgeType::EDGECONST, PhabricatorObjectMentionedByObjectEdgeType::EDGECONST, PhabricatorObjectMentionsObjectEdgeType::EDGECONST, ); $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes($edge_types); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $phids = array_keys($phids); $handles = $viewer->loadHandles($phids); $timeline = $this->buildTransactionTimeline( $task, new ManiphestTransactionQuery()); $monogram = $task->getMonogram(); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($monogram) ->setBorder(true); $header = $this->buildHeaderView($task); $details = $this->buildPropertyView($task, $field_list, $edges, $handles); $description = $this->buildDescriptionView($task); $curtain = $this->buildCurtain($task, $edit_engine); $title = pht('%s %s', $monogram, $task->getTitle()); $comment_view = $edit_engine ->buildEditEngineCommentView($task); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); $related_tabs = array(); $graph_menu = null; $graph_limit = 100; $task_graph = id(new ManiphestTaskGraph()) ->setViewer($viewer) ->setSeedPHID($task->getPHID()) ->setLimit($graph_limit) ->loadGraph(); if (!$task_graph->isEmpty()) { $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $parent_map = $task_graph->getEdges($parent_type); $subtask_map = $task_graph->getEdges($subtask_type); $parent_list = idx($parent_map, $task->getPHID(), array()); $subtask_list = idx($subtask_map, $task->getPHID(), array()); $has_parents = (bool)$parent_list; $has_subtasks = (bool)$subtask_list; $search_text = pht('Search...'); // First, get a count of direct parent tasks and subtasks. If there // are too many of these, we just don't draw anything. You can use // the search button to browse tasks with the search UI instead. $direct_count = count($parent_list) + count($subtask_list); if ($direct_count > $graph_limit) { $message = pht( 'Task graph too large to display (this task is directly connected '. 'to more than %s other tasks). Use %s to explore connected tasks.', $graph_limit, phutil_tag('strong', array(), $search_text)); $message = phutil_tag('em', array(), $message); $graph_table = id(new PHUIPropertyListView()) ->addTextContent($message); } else { // If there aren't too many direct tasks, but there are too many total // tasks, we'll only render directly connected tasks. if ($task_graph->isOverLimit()) { $task_graph->setRenderOnlyAdjacentNodes(true); } $graph_table = $task_graph->newGraphTable(); } $parents_uri = urisprintf( '/?subtaskIDs=%d#R', $task->getID()); $parents_uri = $this->getApplicationURI($parents_uri); $subtasks_uri = urisprintf( '/?parentIDs=%d#R', $task->getID()); $subtasks_uri = $this->getApplicationURI($subtasks_uri); $dropdown_menu = id(new PhabricatorActionListView()) ->setViewer($viewer) ->addAction( id(new PhabricatorActionView()) ->setHref($parents_uri) ->setName(pht('Search Parent Tasks')) ->setDisabled(!$has_parents) ->setIcon('fa-chevron-circle-up')) ->addAction( id(new PhabricatorActionView()) ->setHref($subtasks_uri) ->setName(pht('Search Subtasks')) ->setDisabled(!$has_subtasks) ->setIcon('fa-chevron-circle-down')); $graph_menu = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-search') ->setText($search_text) ->setDropdownMenu($dropdown_menu); $related_tabs[] = id(new PHUITabView()) ->setName(pht('Task Graph')) ->setKey('graph') ->appendChild($graph_table); } $related_tabs[] = $this->newMocksTab($task, $query); $related_tabs[] = $this->newMentionsTab($task, $query); $tab_view = null; $related_tabs = array_filter($related_tabs); if ($related_tabs) { $tab_group = new PHUITabGroupView(); foreach ($related_tabs as $tab) { $tab_group->addTab($tab); } $related_header = id(new PHUIHeaderView()) ->setHeader(pht('Related Objects')); if ($graph_menu) { $related_header->addActionLink($graph_menu); } $tab_view = id(new PHUIObjectBoxView()) ->setHeader($related_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); } $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $tab_view, $timeline, $comment_view, )) ->addPropertySection(pht('Description'), $description) ->addPropertySection(pht('Details'), $details); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs( array( $task->getPHID(), )) ->appendChild($view); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $priority_name = ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()); $priority_color = ManiphestTaskPriority::getTaskPriorityColor( $task->getPriority()); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription( - $status, $priority_name, $priority_color); + $status, $priority_name); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); $view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon( $task->getStatus()).' '.$priority_color); if (ManiphestTaskPoints::getIsEnabled()) { $points = $task->getPoints(); if ($points !== null) { $points_name = pht('%s %s', $task->getPoints(), ManiphestTaskPoints::getPointsLabel()); $tag = id(new PHUITagView()) ->setName($points_name) ->setShade('blue') ->setType(PHUITagView::TYPE_SHADE); $view->addTag($tag); } } return $view; } private function buildCurtain( ManiphestTask $task, PhabricatorEditEngine $edit_engine) { $viewer = $this->getViewer(); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($task); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $edit_config = $edit_engine->loadDefaultEditConfiguration(); $can_create = (bool)$edit_config; $can_reassign = $edit_engine->hasEditAccessToTransaction( ManiphestTransaction::TYPE_OWNER); if ($can_create) { $form_key = $edit_config->getIdentifier(); $edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) ->setQueryParam('parent', $id) ->setQueryParam('template', $id) ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $edit_uri = $this->getApplicationURI($edit_uri); } else { // TODO: This will usually give us a somewhat-reasonable error page, but // could be a bit cleaner. $edit_uri = "/task/edit/{$id}/"; $edit_uri = $this->getApplicationURI($edit_uri); } $subtask_item = id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($edit_uri) ->setIcon('fa-level-down') ->setDisabled(!$can_create) ->setWorkflow(!$can_create); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $task); $submenu_actions = array( $subtask_item, ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY, ); $task_submenu = $relationship_list->newActionSubmenu($submenu_actions) ->setName(pht('Edit Related Tasks...')) ->setIcon('fa-anchor'); $curtain->addAction($task_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } $owner_phid = $task->getOwnerPHID(); $author_phid = $task->getAuthorPHID(); $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); if ($owner_phid) { $image_uri = $handles[$owner_phid]->getImageURI(); $image_href = $handles[$owner_phid]->getURI(); $owner = $viewer->renderHandle($owner_phid)->render(); $content = phutil_tag('strong', array(), $owner); $assigned_to = id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } else { $assigned_to = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Assigned To')) ->appendChild($assigned_to); $author_uri = $handles[$author_phid]->getImageURI(); $author_href = $handles[$author_phid]->getURI(); $author = $viewer->renderHandle($author_phid)->render(); $content = phutil_tag('strong', array(), $author); $date = phabricator_date($task->getDateCreated(), $viewer); $content = pht('%s, %s', $content, $date); $authored_by = id(new PHUIHeadThingView()) ->setImage($author_uri) ->setImageHref($author_href) ->setContent($content); $curtain->newPanel() ->setHeaderText(pht('Authored By')) ->appendChild($authored_by); return $curtain; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, $handles) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); $view->addProperty( pht('From Email'), phutil_tag( 'a', array( 'href' => 'mailto:'.$source.'?subject='.$subject, ), $source)); } $edge_types = array( ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), ); $revisions_commits = array(); $commit_phids = array_keys( $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); if ($commit_phids) { $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST; $drev_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($commit_phids) ->withEdgeTypes(array($commit_drev)) ->execute(); foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles->renderHandle($phid) ->setShowHovercard(true) ->setShowStateIcon(true); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = $handles->getHandleIfExists($revision_phid); if ($revision_handle) { $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; unset($edges[$task_drev][$revision_phid]); $revisions_commits[$phid] = hsprintf( '%s / %s', $revision_handle->renderHovercardLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if (!$edges[$edge_type]) { continue; } $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); $edge_list = $edge_handles->renderList() ->setShowStateIcons(true); $view->addProperty($edge_name, $edge_list); } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $field_list->appendFieldsToPropertyList( $task, $viewer, $view); if ($view->hasAnyProperties()) { return $view; } return null; } private function buildDescriptionView(ManiphestTask $task) { $viewer = $this->getViewer(); $section = null; $description = $task->getDescription(); if (strlen($description)) { $section = new PHUIPropertyListView(); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), id(new PHUIRemarkupView($viewer, $description)) ->setContextObject($task))); } return $section; } private function newMocksTab( ManiphestTask $task, PhabricatorEdgeQuery $edge_query) { $mock_type = ManiphestTaskHasMockEdgeType::EDGECONST; $mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type)); if (!$mock_phids) { return null; } $viewer = $this->getViewer(); $handles = $viewer->loadHandles($mock_phids); // TODO: It would be nice to render this as pinboard-style thumbnails, // similar to "{M123}", instead of a list of links. $view = id(new PHUIPropertyListView()) ->addProperty(pht('Mocks'), $handles->renderList()); return id(new PHUITabView()) ->setName(pht('Mocks')) ->setKey('mocks') ->appendChild($view); } private function newMentionsTab( ManiphestTask $task, PhabricatorEdgeQuery $edge_query) { $in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST; $out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST; $in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type)); $out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type)); // Filter out any mentioned users from the list. These are not generally // very interesting to show in a relationship summary since they usually // end up as subscribers anyway. $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($out_phids as $key => $out_phid) { if (phid_get_type($out_phid) == $user_type) { unset($out_phids[$key]); } } if (!$in_phids && !$out_phids) { return null; } $viewer = $this->getViewer(); $in_handles = $viewer->loadHandles($in_phids); $out_handles = $viewer->loadHandles($out_phids); $in_handles = $this->getCompleteHandles($in_handles); $out_handles = $this->getCompleteHandles($out_handles); if (!count($in_handles) && !count($out_handles)) { return null; } $view = new PHUIPropertyListView(); if (count($in_handles)) { $view->addProperty(pht('Mentioned In'), $in_handles->renderList()); } if (count($out_handles)) { $view->addProperty(pht('Mentioned Here'), $out_handles->renderList()); } return id(new PHUITabView()) ->setName(pht('Mentions')) ->setKey('mentions') ->appendChild($view); } private function getCompleteHandles(PhabricatorHandleList $handles) { $phids = array(); foreach ($handles as $phid => $handle) { if (!$handle->isComplete()) { continue; } $phids[] = $phid; } return $handles->newSublist($phids); } } diff --git a/src/applications/maniphest/field/ManiphestConfiguredCustomField.php b/src/applications/maniphest/field/ManiphestConfiguredCustomField.php index 9e6b4f038b..68c20c70bc 100644 --- a/src/applications/maniphest/field/ManiphestConfiguredCustomField.php +++ b/src/applications/maniphest/field/ManiphestConfiguredCustomField.php @@ -1,22 +1,21 @@ mailer = new PHPMailer($use_exceptions = true); $this->mailer->CharSet = 'utf-8'; - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding', '8bit'); + $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); $this->mailer->Encoding = $encoding; // By default, PHPMailer sends one mail per recipient. We handle // multiplexing higher in the stack, so tell it to send mail exactly // like we ask. $this->mailer->SingleTo = false; $mailer = PhabricatorEnv::getEnvConfig('phpmailer.mailer'); if ($mailer == 'smtp') { $this->mailer->IsSMTP(); $this->mailer->Host = PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'); $this->mailer->Port = PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'); $user = PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'); if ($user) { $this->mailer->SMTPAuth = true; $this->mailer->Username = $user; $this->mailer->Password = PhabricatorEnv::getEnvConfig('phpmailer.smtp-password'); } $protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'); if ($protocol) { $protocol = phutil_utf8_strtolower($protocol); $this->mailer->SMTPSecure = $protocol; } } else if ($mailer == 'sendmail') { $this->mailer->IsSendmail(); } else { // Do nothing, by default PHPMailer send message using PHP mail() // function. } } public function supportsMessageIDHeader() { return true; } public function setFrom($email, $name = '') { $this->mailer->SetFrom($email, $name, $crazy_side_effects = false); return $this; } public function addReplyTo($email, $name = '') { $this->mailer->AddReplyTo($email, $name); return $this; } public function addTos(array $emails) { foreach ($emails as $email) { $this->mailer->AddAddress($email); } return $this; } public function addCCs(array $emails) { foreach ($emails as $email) { $this->mailer->AddCC($email); } return $this; } public function addAttachment($data, $filename, $mimetype) { $this->mailer->AddStringAttachment( $data, $filename, 'base64', $mimetype); return $this; } public function addHeader($header_name, $header_value) { if (strtolower($header_name) == 'message-id') { $this->mailer->MessageID = $header_value; } else { $this->mailer->AddCustomHeader($header_name.': '.$header_value); } return $this; } public function setBody($body) { $this->mailer->IsHTML(false); $this->mailer->Body = $body; return $this; } public function setHTMLBody($html_body) { $this->mailer->IsHTML(true); $this->mailer->Body = $html_body; return $this; } public function setSubject($subject) { $this->mailer->Subject = $subject; return $this; } public function hasValidRecipients() { return true; } public function send() { return $this->mailer->Send(); } } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php index 2a8b12b64a..f072e769c3 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php @@ -1,107 +1,107 @@ mailer = new PHPMailerLite($use_exceptions = true); $this->mailer->CharSet = 'utf-8'; - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding', '8bit'); + $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); $this->mailer->Encoding = $encoding; // By default, PHPMailerLite sends one mail per recipient. We handle // multiplexing higher in the stack, so tell it to send mail exactly // like we ask. $this->mailer->SingleTo = false; } public function supportsMessageIDHeader() { return true; } public function setFrom($email, $name = '') { $this->mailer->SetFrom($email, $name, $crazy_side_effects = false); return $this; } public function addReplyTo($email, $name = '') { $this->mailer->AddReplyTo($email, $name); return $this; } public function addTos(array $emails) { foreach ($emails as $email) { $this->mailer->AddAddress($email); } return $this; } public function addCCs(array $emails) { foreach ($emails as $email) { $this->mailer->AddCC($email); } return $this; } public function addAttachment($data, $filename, $mimetype) { $this->mailer->AddStringAttachment( $data, $filename, 'base64', $mimetype); return $this; } public function addHeader($header_name, $header_value) { if (strtolower($header_name) == 'message-id') { $this->mailer->MessageID = $header_value; } else { $this->mailer->AddCustomHeader($header_name.': '.$header_value); } return $this; } public function setBody($body) { $this->mailer->Body = $body; $this->mailer->IsHTML(false); return $this; } /** * Note: phpmailer-lite does NOT support sending messages with mixed version * (plaintext and html). So for now lets just use HTML if it's available. * @param $html */ public function setHTMLBody($html_body) { $this->mailer->Body = $html_body; $this->mailer->IsHTML(true); return $this; } public function setSubject($subject) { $this->mailer->Subject = $subject; return $this; } public function hasValidRecipients() { return true; } public function send() { return $this->mailer->Send(); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php index 96d2f50f0a..b26bb0b8a7 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php @@ -1,56 +1,56 @@ setData($data); $this->setFilename($filename); $this->setMimeType($mimetype); } public function getData() { return $this->data; } public function setData($data) { $this->data = $data; return $this; } public function getFilename() { return $this->filename; } public function setFilename($filename) { $this->filename = $filename; return $this; } public function getMimeType() { return $this->mimetype; } public function setMimeType($mimetype) { $this->mimetype = $mimetype; return $this; } public function toDictionary() { return array( 'filename' => $this->getFilename(), - 'mimetype' => $this->getMimetype(), + 'mimetype' => $this->getMimeType(), 'data' => $this->getData(), ); } public static function newFromDictionary(array $dict) { return new PhabricatorMetaMTAAttachment( idx($dict, 'data'), idx($dict, 'filename'), idx($dict, 'mimetype')); } } diff --git a/src/applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php b/src/applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php index a35296311f..092ebf41cc 100644 --- a/src/applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php +++ b/src/applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php @@ -1,23 +1,21 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withParentPHIDs(array $phids) { $this->parentPHIDs = $phids; return $this; } public function needContent($need_content) { $this->needContent = $need_content; return $this; } public function needRawContent($need_raw_content) { $this->needRawContent = $need_raw_content; return $this; } public function needSnippets($need_snippets) { $this->needSnippets = $need_snippets; return $this; } public function withLanguages(array $languages) { $this->includeNoLanguage = false; foreach ($languages as $key => $language) { if ($language === null) { $languages[$key] = ''; continue; } } $this->languages = $languages; return $this; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function newResultObject() { return new PhabricatorPaste(); } protected function loadPage() { return $this->loadStandardPage(new PhabricatorPaste()); } protected function didFilterPage(array $pastes) { if ($this->needRawContent) { $pastes = $this->loadRawContent($pastes); } if ($this->needContent) { $pastes = $this->loadContent($pastes); } if ($this->needSnippets) { $pastes = $this->loadSnippets($pastes); } return $pastes; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, 'parentPHID IN (%Ls)', $this->parentPHIDs); } if ($this->languages !== null) { $where[] = qsprintf( $conn, 'language IN (%Ls)', $this->languages); } if ($this->dateCreatedAfter !== null) { $where[] = qsprintf( $conn, 'dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore !== null) { $where[] = qsprintf( $conn, 'dateCreated <= %d', $this->dateCreatedBefore); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } return $where; } private function getContentCacheKey(PhabricatorPaste $paste) { return implode( ':', array( 'P'.$paste->getID(), $paste->getFilePHID(), $paste->getLanguage(), PhabricatorHash::digestForIndex($paste->getTitle()), )); } private function getSnippetCacheKey(PhabricatorPaste $paste) { return implode( ':', array( 'P'.$paste->getID(), $paste->getFilePHID(), $paste->getLanguage(), 'snippet', 'v2', PhabricatorHash::digestForIndex($paste->getTitle()), )); } private function loadRawContent(array $pastes) { $file_phids = mpull($pastes, 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($pastes as $key => $paste) { $file = idx($files, $paste->getFilePHID()); if (!$file) { unset($pastes[$key]); continue; } try { $paste->attachRawContent($file->loadFileData()); } catch (Exception $ex) { // We can hit various sorts of file storage issues here. Just drop the // paste if the file is dead. unset($pastes[$key]); continue; } } return $pastes; } private function loadContent(array $pastes) { $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); $keys = array(); foreach ($pastes as $paste) { $keys[] = $this->getContentCacheKey($paste); } $caches = $cache->getKeys($keys); $need_raw = array(); $have_cache = array(); foreach ($pastes as $paste) { $key = $this->getContentCacheKey($paste); if (isset($caches[$key])) { $paste->attachContent(phutil_safe_html($caches[$key])); $have_cache[$paste->getPHID()] = true; } else { $need_raw[$key] = $paste; } } if (!$need_raw) { return $pastes; } $write_data = array(); $have_raw = $this->loadRawContent($need_raw); $have_raw = mpull($have_raw, null, 'getPHID'); foreach ($pastes as $key => $paste) { $paste_phid = $paste->getPHID(); if (isset($have_cache[$paste_phid])) { continue; } if (empty($have_raw[$paste_phid])) { unset($pastes[$key]); continue; } $content = $this->buildContent($paste); $paste->attachContent($content); $write_data[$this->getContentCacheKey($paste)] = (string)$content; } if ($write_data) { $cache->setKeys($write_data); } return $pastes; } private function loadSnippets(array $pastes) { $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); $keys = array(); foreach ($pastes as $paste) { $keys[] = $this->getSnippetCacheKey($paste); } $caches = $cache->getKeys($keys); $need_raw = array(); $have_cache = array(); foreach ($pastes as $paste) { $key = $this->getSnippetCacheKey($paste); if (isset($caches[$key])) { - $snippet_data = phutil_json_decode($caches[$key], true); + $snippet_data = phutil_json_decode($caches[$key]); $snippet = new PhabricatorPasteSnippet( phutil_safe_html($snippet_data['content']), $snippet_data['type'], $snippet_data['contentLineCount']); $paste->attachSnippet($snippet); $have_cache[$paste->getPHID()] = true; } else { $need_raw[$key] = $paste; } } if (!$need_raw) { return $pastes; } $write_data = array(); $have_raw = $this->loadRawContent($need_raw); $have_raw = mpull($have_raw, null, 'getPHID'); foreach ($pastes as $key => $paste) { $paste_phid = $paste->getPHID(); if (isset($have_cache[$paste_phid])) { continue; } if (empty($have_raw[$paste_phid])) { unset($pastes[$key]); continue; } $snippet = $this->buildSnippet($paste); $paste->attachSnippet($snippet); $snippet_data = array( 'content' => (string)$snippet->getContent(), 'type' => (string)$snippet->getType(), 'contentLineCount' => $snippet->getContentLineCount(), ); $write_data[$this->getSnippetCacheKey($paste)] = phutil_json_encode( $snippet_data); } if ($write_data) { $cache->setKeys($write_data); } return $pastes; } private function buildContent(PhabricatorPaste $paste) { return $this->highlightSource( $paste->getRawContent(), $paste->getTitle(), $paste->getLanguage()); } private function buildSnippet(PhabricatorPaste $paste) { $snippet_type = PhabricatorPasteSnippet::FULL; $snippet = $paste->getRawContent(); if (strlen($snippet) > 1024) { $snippet_type = PhabricatorPasteSnippet::FIRST_BYTES; $snippet = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(1024) ->setTerminator('') ->truncateString($snippet); } $lines = phutil_split_lines($snippet); $line_count = count($lines); if ($line_count > 5) { $snippet_type = PhabricatorPasteSnippet::FIRST_LINES; $snippet = implode('', array_slice($lines, 0, 5)); } return new PhabricatorPasteSnippet( $this->highlightSource( $snippet, $paste->getTitle(), $paste->getLanguage()), $snippet_type, $line_count); } private function highlightSource($source, $title, $language) { if (empty($language)) { return PhabricatorSyntaxHighlighter::highlightWithFilename( $title, $source); } else { return PhabricatorSyntaxHighlighter::highlightWithLanguage( $language, $source); } } public function getQueryApplicationClass() { return 'PhabricatorPasteApplication'; } } diff --git a/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php b/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php index 931a78df96..56fc913558 100644 --- a/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php +++ b/src/applications/paste/xaction/PhabricatorPasteTitleTransaction.php @@ -1,65 +1,65 @@ getTitle(); } public function applyInternalEffects($object, $value) { $object->setTitle($value); } public function getTitle() { $old = $this->getOldValue(); - $new = $this->getNeWValue(); + $new = $this->getNewValue(); if (strlen($old) && strlen($new)) { return pht( '%s changed the title of this paste from %s to %s.', $this->renderAuthor(), $this->renderOldValue(), $this->renderNewValue()); } else if (strlen($new)) { return pht( '%s changed the title of this paste from untitled to %s.', $this->renderAuthor(), $this->renderNewValue()); } else { return pht( '%s changed the title of this paste from %s to untitled.', $this->renderAuthor(), $this->renderOldValue()); } } public function getTitleForFeed() { $old = $this->getOldValue(); - $new = $this->getNeWValue(); + $new = $this->getNewValue(); if (strlen($old) && strlen($new)) { return pht( '%s updated the title for %s from %s to %s.', $this->renderAuthor(), $this->renderObject(), $this->renderOldValue(), $this->renderNewValue()); } else if (strlen($new)) { return pht( '%s updated the title for %s from untitled to %s.', $this->renderAuthor(), $this->renderObject(), $this->renderNewValue()); } else { return pht( '%s updated the title for %s from %s to untitled.', $this->renderAuthor(), $this->renderObject(), $this->renderOldValue()); } } } diff --git a/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php b/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php index 9c58b970bb..800ea320c3 100644 --- a/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php +++ b/src/applications/people/customfield/PhabricatorUserConfiguredCustomField.php @@ -1,29 +1,29 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->attachBlog($blog) ->setDatePublished(PhabricatorTime::getNow()) ->setVisibility(PhameConstants::VISIBILITY_PUBLISHED); return $post; } public function attachBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->assertAttached($this->blog); } public function getMonogram() { return 'J'.$this->getID(); } public function getLiveURI() { $blog = $this->getBlog(); $is_draft = $this->isDraft(); $is_archived = $this->isArchived(); if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } public function getExternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $path = "/post/{$id}/{$slug}/"; $domain = $this->getBlog()->getDomain(); return (string)id(new PhutilURI('http://'.$domain)) ->setPath($path); } public function getInternalLiveURI() { $id = $this->getID(); $slug = $this->getSlug(); $blog_id = $this->getBlog()->getID(); return "/phame/live/{$blog_id}/post/{$id}/{$slug}/"; } public function getViewURI() { $id = $this->getID(); $slug = $this->getSlug(); return "/phame/post/view/{$id}/{$slug}/"; } public function getBestURI($is_live, $is_external) { if ($is_live) { if ($is_external) { return $this->getExternalLiveURI(); } else { return $this->getInternalLiveURI(); } } else { return $this->getViewURI(); } } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT); } public function isArchived() { return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'subtitle' => 'text64', 'phameTitle' => 'sort64?', 'visibility' => 'uint32', 'mailKey' => 'bytes20', 'headerImagePHID' => 'phid?', // T6203/NULLABILITY // These seem like they should always be non-null? 'blogPHID' => 'phid?', 'body' => 'text?', 'configData' => 'text?', // T6203/NULLABILITY // This one probably should be nullable? 'datePublished' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'bloggerPosts' => array( 'columns' => array( 'bloggerPHID', 'visibility', 'datePublished', 'id', ), ), ), ) + parent::getConfiguration(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePostPHIDType::TYPECONST); } public function getSlug() { - return PhabricatorSlug::normalizeProjectSlug($this->getTitle(), true); + return PhabricatorSlug::normalizeProjectSlug($this->getTitle()); } public function getHeaderImageURI() { return $this->getHeaderImageFile()->getBestURI(); } public function attachHeaderImageFile(PhabricatorFile $file) { $this->headerImageFile = $file; return $this; } public function getHeaderImageFile() { return $this->assertAttached($this->headerImageFile); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft posts are visible only to the author. Published posts are visible // to whoever the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getBlog()) { return $this->getBlog()->getEditPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht('The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhamePostEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhamePostTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->bloggerPHID == $phid); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('Title of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('slug') ->setType('string') ->setDescription(pht('Slug for the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('blogPHID') ->setType('phid') ->setDescription(pht('PHID of the blog that the post belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('PHID of the author of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('body') ->setType('string') ->setDescription(pht('Body of the post.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('datePublished') ->setType('epoch?') ->setDescription(pht('Publish date, if the post has been published.')), ); } public function getFieldValuesForConduit() { if ($this->isDraft()) { $date_published = null; } else if ($this->isArchived()) { $date_published = null; } else { $date_published = (int)$this->getDatePublished(); } return array( 'title' => $this->getTitle(), 'slug' => $this->getSlug(), 'blogPHID' => $this->getBlogPHID(), 'authorPHID' => $this->getBloggerPHID(), 'body' => $this->getBody(), 'datePublished' => $date_published, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhamePostFulltextEngine(); } } diff --git a/src/applications/phid/utils.php b/src/applications/phid/utils.php index d667473190..152a547284 100644 --- a/src/applications/phid/utils.php +++ b/src/applications/phid/utils.php @@ -1,38 +1,38 @@ list of phids */ function phid_group_by_type($phids) { $result = array(); foreach ($phids as $phid) { $type = phid_get_type($phid); $result[$type][] = $phid; } return $result; } function phid_get_subtype($phid) { if (isset($phid[14]) && ($phid[14] == '-')) { return substr($phid, 10, 4); } return null; } diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index a1fb40c86b..2ab31d0d96 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -1,238 +1,238 @@ maniphestTaskPHIDs = $maniphest_task_phids; return $this; } private function getManiphestTaskPHIDs() { return $this->maniphestTaskPHIDs; } public function shouldAllowPublic() { return true; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); $image_id = $request->getURIData('imageID'); $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needImages(true) ->needInlineComments(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PholioMockHasTaskEdgeType::EDGECONST); $this->setManiphestTaskPHIDs($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); $title = $mock->getName(); if ($mock->isClosed()) { $header_icon = 'fa-ban'; $header_name = pht('Closed'); $header_color = 'dark'; } else { $header_icon = 'fa-square-o'; $header_name = pht('Open'); $header_color = 'bluegrey'; } $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setStatus($header_icon, $header_color, $header_name) ->setPolicyObject($mock) ->setHeaderIcon('fa-camera-retro'); $timeline = $this->buildTransactionTimeline( $mock, new PholioTransactionQuery(), $engine); $timeline->setMock($mock); $curtain = $this->buildCurtainView($mock); - $details = $this->buildDescriptionView($mock, $engine); + $details = $this->buildDescriptionView($mock); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); $comment_form_id = celerity_generate_unique_node_id(); $mock_view = id(new PholioMockImagesView()) ->setRequestURI($request->getRequestURI()) ->setCommentFormID($comment_form_id) ->setUser($viewer) ->setMock($mock) ->setImageID($image_id); $output = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($mock_view); $add_comment = $this->buildAddCommentView($mock, $comment_form_id); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); $crumbs->setBorder(true); $thumb_grid = id(new PholioMockThumbGridView()) ->setUser($viewer) ->setMock($mock); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $output, $thumb_grid, $details, $timeline, $add_comment, )); return $this->newPage() ->setTitle('M'.$mock->getID().' '.$title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($mock->getPHID())) ->addQuicksandConfig( array('mockViewConfig' => $mock_view->getBehaviorConfig())) ->appendChild($view); } private function buildCurtainView(PholioMock $mock) { $viewer = $this->getViewer(); $curtain = $this->newCurtainView($mock); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $mock, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Mock')) ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($mock->isClosed()) { $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-check') ->setName(pht('Open Mock')) ->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setName(pht('Close Mock')) ->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $mock); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } if ($this->getManiphestTaskPHIDs()) { $curtain->newPanel() ->setHeaderText(pht('Maniphest Tasks')) ->appendChild( $viewer->renderHandleList($this->getManiphestTaskPHIDs())); } $curtain->newPanel() ->setHeaderText(pht('Authored By')) ->appendChild($this->buildAuthorPanel($mock)); return $curtain; } private function buildDescriptionView(PholioMock $mock) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); $description = $mock->getDescription(); if (strlen($description)) { $properties->addTextContent( new PHUIRemarkupView($viewer, $description)); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Mock Description')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } return null; } private function buildAuthorPanel(PholioMock $mock) { $viewer = $this->getViewer(); $author_phid = $mock->getAuthorPHID(); $handles = $viewer->loadHandles(array($author_phid)); $author_uri = $handles[$author_phid]->getImageURI(); $author_href = $handles[$author_phid]->getURI(); $author = $viewer->renderHandle($author_phid)->render(); $content = phutil_tag('strong', array(), $author); $date = phabricator_date($mock->getDateCreated(), $viewer); $content = pht('%s, %s', $content, $date); $authored_by = id(new PHUIHeadThingView()) ->setImage($author_uri) ->setImageHref($author_href) ->setContent($content); return $authored_by; } private function buildAddCommentView(PholioMock $mock, $comment_form_id) { $viewer = $this->getViewer(); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $mock->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('History Beckons'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($mock->getPHID()) ->setFormID($comment_form_id) ->setDraft($draft) ->setHeaderText($title) ->setSubmitButtonName(pht('Add Comment')) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php index af733ab730..0f04783419 100644 --- a/src/applications/phragment/storage/PhragmentFragment.php +++ b/src/applications/phragment/storage/PhragmentFragment.php @@ -1,348 +1,349 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text128', 'depth' => 'uint32', 'latestVersionPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_path' => array( 'columns' => array('path'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentFragmentPHIDType::TYPECONST); } public function getURI() { return '/phragment/browse/'.$this->getPath(); } public function getName() { return basename($this->path); } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function isDirectory() { return $this->latestVersionPHID === null; } public function isDeleted() { return $this->getLatestVersion()->getFilePHID() === null; } public function getLatestVersion() { if ($this->latestVersionPHID === null) { return null; } return $this->assertAttached($this->latestVersion); } public function attachLatestVersion(PhragmentFragmentVersion $version) { return $this->latestVersion = $version; } /* -( Updating ) --------------------------------------------------------- */ /** * Create a new fragment from a file. */ public static function createFromFile( PhabricatorUser $viewer, PhabricatorFile $file = null, $path = null, $view_policy = null, $edit_policy = null) { $fragment = id(new PhragmentFragment()); $fragment->setPath($path); $fragment->setDepth(count(explode('/', $path))); $fragment->setLatestVersionPHID(null); $fragment->setViewPolicy($view_policy); $fragment->setEditPolicy($edit_policy); $fragment->save(); // Directory fragments have no versions associated with them, so we // just return the fragment at this point. if ($file === null) { return $fragment; } if ($file->getMimeType() === 'application/zip') { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return $fragment; } /** * Set the specified file as the next version for the fragment. */ public function updateFromFile( PhabricatorUser $viewer, PhabricatorFile $file) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID($file->getPHID()); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); $file->attachToObject($version->getPHID()); } /** * Apply the specified ZIP archive onto the fragment, removing * and creating fragments as needed. */ public function updateFromZIP( PhabricatorUser $viewer, PhabricatorFile $file) { if ($file->getMimeType() !== 'application/zip') { throw new Exception( pht("File must have mimetype '%s'.", 'application/zip')); } // First apply the ZIP as normal. $this->updateFromFile($viewer, $file); // Ensure we have ZIP support. $zip = null; try { $zip = new ZipArchive(); } catch (Exception $e) { // The server doesn't have php5-zip, so we can't do recursive updates. return; } $temp = new TempFile(); Filesystem::writeFile($temp, $file->loadFileData()); if (!$zip->open($temp)) { throw new Exception(pht('Unable to open ZIP.')); } // Get all of the paths and their data from the ZIP. $mappings = array(); for ($i = 0; $i < $zip->numFiles; $i++) { $path = trim($zip->getNameIndex($i), '/'); $stream = $zip->getStream($path); $data = null; // If the stream is false, then it is a directory entry. We leave // $data set to null for directories so we know not to create a // version entry for them. if ($stream !== false) { $data = stream_get_contents($stream); fclose($stream); } $mappings[$path] = $data; } // We need to detect any directories that are in the ZIP folder that // aren't explicitly noted in the ZIP. This can happen if the file // entries in the ZIP look like: // // * something/blah.png // * something/other.png // * test.png // // Where there is no explicit "something/" entry. foreach ($mappings as $path_key => $data) { if ($data === null) { continue; } $directory = dirname($path_key); while ($directory !== '.') { if (!array_key_exists($directory, $mappings)) { $mappings[$directory] = null; } if (dirname($directory) === $directory) { // dirname() will not reduce this directory any further; to // prevent infinite loop we just break out here. break; } $directory = dirname($directory); } } // Adjust the paths relative to this fragment so we can look existing // fragments up in the DB. $base_path = $this->getPath(); $paths = array(); foreach ($mappings as $p => $data) { $paths[] = $base_path.'/'.$p; } // FIXME: What happens when a child exists, but the current user // can't see it. We're going to create a new child with the exact // same path and then bad things will happen. $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->execute(); $children = mpull($children, null, 'getPath'); // Iterate over the existing fragments. foreach ($children as $full_path => $child) { $path = substr($full_path, strlen($base_path) + 1); if (array_key_exists($path, $mappings)) { if ($child->isDirectory() && $mappings[$path] === null) { // Don't create a version entry for a directory // (unless it's been converted into a file). continue; } // The file is being updated. $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); $child->updateFromFile($viewer, $file); } else { // The file is being deleted. $child->deleteFile($viewer); } } // Iterate over the mappings to find new files. foreach ($mappings as $path => $data) { if (!array_key_exists($base_path.'/'.$path, $children)) { // The file is being created. If the data is null, // then this is explicitly a directory being created. $file = null; if ($mappings[$path] !== null) { $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); } self::createFromFile( $viewer, $file, $base_path.'/'.$path, $this->getViewPolicy(), $this->getEditPolicy()); } } } /** * Delete the contents of the specified fragment. */ public function deleteFile(PhabricatorUser $viewer) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID(null); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); } /* -( Utility ) ---------------------------------------------------------- */ public function getFragmentMappings( PhabricatorUser $viewer, $base_path) { $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->withDepths(array($this->getDepth() + 1)) ->execute(); if (count($children) === 0) { $path = substr($this->getPath(), strlen($base_path) + 1); return array($path => $this); } else { $mappings = array(); foreach ($children as $child) { $child_mappings = $child->getFragmentMappings( $viewer, $base_path); foreach ($child_mappings as $key => $value) { $mappings[$key] = $value; } } return $mappings; } } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php index 60d9ece734..360b3e7b93 100644 --- a/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php +++ b/src/applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php @@ -1,46 +1,44 @@ 'required string', ); } protected function defineReturnType() { return 'nonempty dict'; } protected function defineErrorTypes() { return array( 'ERR-BAD-DOCUMENT' => pht('No such document exists.'), ); } protected function execute(ConduitAPIRequest $request) { $slug = $request->getValue('slug'); $document = id(new PhrictionDocumentQuery()) ->setViewer($request->getUser()) ->withSlugs(array(PhabricatorSlug::normalize($slug))) ->needContent(true) ->executeOne(); if (!$document) { throw new ConduitException('ERR-BAD-DOCUMENT'); } - return $this->buildDocumentInfoDictionary( - $document, - $document->getContent()); + return $this->buildDocumentInfoDictionary($document); } } diff --git a/src/applications/policy/controller/PhabricatorPolicyExplainController.php b/src/applications/policy/controller/PhabricatorPolicyExplainController.php index 45ac0ee5e1..da4103765b 100644 --- a/src/applications/policy/controller/PhabricatorPolicyExplainController.php +++ b/src/applications/policy/controller/PhabricatorPolicyExplainController.php @@ -1,326 +1,326 @@ getViewer(); $phid = $request->getURIData('phid'); $capability = $request->getURIData('capability'); $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $policies = PhabricatorPolicyQuery::loadPolicies( $viewer, $object); $policy = idx($policies, $capability); if (!$policy) { return new Aphront404Response(); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); $object_name = $handle->getName(); $object_uri = nonempty($handle->getURI(), '/'); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setClass('aphront-access-dialog aphront-policy-explain-dialog') ->setTitle(pht('Policy Details: %s', $object_name)) ->addCancelButton($object_uri, pht('Done')); $space_section = $this->buildSpaceSection( $object, $policy, $capability); $extended_section = $this->buildExtendedSection( $object, $capability); $exceptions_section = $this->buildExceptionsSection( $object, $capability); $object_section = $this->buildObjectSection( $object, $policy, $capability, $handle); $dialog->appendChild( array( $space_section, $extended_section, $exceptions_section, $object_section, )); return $dialog; } private function buildSpaceSection( PhabricatorPolicyInterface $object, PhabricatorPolicy $policy, $capability) { $viewer = $this->getViewer(); if (!($object instanceof PhabricatorSpacesInterface)) { return null; } - if (!PhabricatorSpacesNamespaceQuery::getSpacesExist($viewer)) { + if (!PhabricatorSpacesNamespaceQuery::getSpacesExist()) { return null; } $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( $object); $spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer); $space = idx($spaces, $space_phid); if (!$space) { return null; } $space_policies = PhabricatorPolicyQuery::loadPolicies($viewer, $space); $space_policy = idx($space_policies, PhabricatorPolicyCapability::CAN_VIEW); if (!$space_policy) { return null; } $doc_href = PhabricatorEnv::getDoclink('Spaces User Guide'); $capability_name = $this->getCapabilityName($capability); $space_section = id(new PHUIPolicySectionView()) ->setViewer($viewer) ->setIcon('fa-th-large bluegrey') ->setHeader(pht('Space')) ->setDocumentationLink(pht('Spaces Documentation'), $doc_href) ->appendList( array( array( phutil_tag('strong', array(), pht('Space:')), ' ', $viewer->renderHandle($space_phid)->setAsTag(true), ), array( phutil_tag('strong', array(), pht('%s:', $capability_name)), ' ', $space_policy->getShortName(), ), )) ->appendParagraph( pht( 'This object is in %s and can only be seen or edited by users '. 'with access to view objects in the space.', $viewer->renderHandle($space_phid))); $space_explanation = PhabricatorPolicy::getPolicyExplanation( $viewer, $space_policy->getPHID()); $items = array(); $items[] = $space_explanation; $space_section ->appendParagraph(pht('Users who can see objects in this space:')) ->appendList($items); $view_capability = PhabricatorPolicyCapability::CAN_VIEW; if ($capability == $view_capability) { $stronger = $space_policy->isStrongerThan($policy); if ($stronger) { $space_section->appendHint( pht( 'The space this object is in has a more restrictive view '. 'policy ("%s") than the object does ("%s"), so the space\'s '. 'view policy is shown as a hint instead of the object policy.', $space_policy->getShortName(), $policy->getShortName())); } } $space_section->appendHint( pht( 'After a user passes space policy checks, they must still pass '. 'object policy checks.')); return $space_section; } private function getStrengthInformation( PhabricatorPolicyInterface $object, PhabricatorPolicy $policy, $capability) { $viewer = $this->getViewer(); $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject( $viewer, $object, $capability); if (!$default_policy) { return; } if ($default_policy->getPHID() == $policy->getPHID()) { return; } if ($default_policy->isStrongerThan($policy)) { $info = pht( 'This object has a less restrictive policy ("%s") than the default '. 'policy for similar objects (which is "%s").', $policy->getShortName(), $default_policy->getShortName()); } else if ($policy->isStrongerThan($default_policy)) { $info = pht( 'This object has a more restrictive policy ("%s") than the default '. 'policy for similar objects (which is "%s").', $policy->getShortName(), $default_policy->getShortName()); } else { $info = pht( 'This object has a different policy ("%s") than the default policy '. 'for similar objects (which is "%s").', $policy->getShortName(), $default_policy->getShortName()); } return $info; } private function getCapabilityName($capability) { $capability_name = $capability; $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { $capability_name = $capobj->getCapabilityName(); } return $capability_name; } private function buildExtendedSection( PhabricatorPolicyInterface $object, $capability) { $viewer = $this->getViewer(); if (!($object instanceof PhabricatorExtendedPolicyInterface)) { return null; } $extended_rules = $object->getExtendedPolicy($capability, $viewer); if (!$extended_rules) { return null; } $items = array(); foreach ($extended_rules as $extended_rule) { $extended_target = $extended_rule[0]; $extended_capabilities = (array)$extended_rule[1]; if (is_object($extended_target)) { $extended_target = $extended_target->getPHID(); } foreach ($extended_capabilities as $extended_capability) { $ex_name = $this->getCapabilityName($extended_capability); $items[] = array( phutil_tag('strong', array(), pht('%s:', $ex_name)), ' ', $viewer->renderHandle($extended_target)->setAsTag(true), ); } } return id(new PHUIPolicySectionView()) ->setViewer($viewer) ->setIcon('fa-link') ->setHeader(pht('Required Capabilities on Other Objects')) ->appendParagraph( pht( 'To access this object, users must have first have access '. 'capabilties on these other objects:')) ->appendList($items); } private function buildExceptionsSection( PhabricatorPolicyInterface $object, $capability) { $viewer = $this->getViewer(); $exceptions = PhabricatorPolicy::getSpecialRules( $object, $viewer, $capability, false); if (!$exceptions) { return null; } return id(new PHUIPolicySectionView()) ->setViewer($viewer) ->setIcon('fa-unlock-alt red') ->setHeader(pht('Special Rules')) ->appendParagraph( pht( 'This object has special rules which override normal object '. 'policy rules:')) ->appendList($exceptions); } private function buildObjectSection( PhabricatorPolicyInterface $object, PhabricatorPolicy $policy, $capability, PhabricatorObjectHandle $handle) { $viewer = $this->getViewer(); $capability_name = $this->getCapabilityName($capability); $object_section = id(new PHUIPolicySectionView()) ->setViewer($viewer) ->setIcon($handle->getIcon().' bluegrey') ->setHeader(pht('Object Policy')) ->appendList( array( array( phutil_tag('strong', array(), pht('%s:', $capability_name)), ' ', $policy->getShortName(), ), )) ->appendParagraph( pht( 'In detail, this means that these users can take this action, '. 'provided they pass all of the checks described above first:')) ->appendList( array( PhabricatorPolicy::getPolicyExplanation( $viewer, $policy->getPHID()), )); $strength = $this->getStrengthInformation($object, $policy, $capability); if ($strength) { $object_section->appendHint($strength); } return $object_section; } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 529c396328..59da519834 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,276 +1,276 @@ getViewer(); $id = $request->getURIData('id'); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needAnswers(true) ->needProjectPHIDs(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $answers = $this->buildAnswers($question); $answer_add_panel = id(new PonderAddAnswerView()) ->setQuestion($question) ->setUser($viewer) ->setActionURI('/ponder/answer/add/'); $header = new PHUIHeaderView(); $header->setHeader($question->getTitle()); $header->setUser($viewer); $header->setPolicyObject($question); $header->setHeaderIcon('fa-university'); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); } else { $text = PonderQuestionStatus::getQuestionStatusFullName( $question->getStatus()); $icon = PonderQuestionStatus::getQuestionStatusIcon( $question->getStatus()); $header->setStatus($icon, 'dark', $text); } $curtain = $this->buildCurtain($question); $details = $this->buildPropertySectionView($question); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $content_id = celerity_generate_unique_node_id(); $timeline = $this->buildTransactionTimeline( $question, id(new PonderQuestionTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $add_comment = phutil_tag_div( 'ponder-question-add-comment-view', $add_comment); $comment_view = phutil_tag( 'div', array( 'id' => $content_id, 'style' => 'display: none;', ), array( $timeline, $add_comment, )); $footer = id(new PonderFooterView()) ->setContentID($content_id) ->setCount(count($xactions)); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); $crumbs->setBorder(true); $subheader = $this->buildSubheaderView($question); $answer_wiki = null; if ($question->getAnswerWiki()) { $wiki = new PHUIRemarkupView($viewer, $question->getAnswerWiki()); $answer_wiki = id(new PHUIObjectBoxView()) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setHeaderText(pht('ANSWER SUMMARY')) ->appendChild($wiki) ->addClass('ponder-answer-wiki'); } require_celerity_resource('ponder-view-css'); $ponder_content = phutil_tag( 'div', array( 'class' => 'ponder-question-content', ), array( $answer_wiki, $footer, $comment_view, $answers, $answer_add_panel, )); $ponder_view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) ->setMainColumn($ponder_content) ->addPropertySection(pht('Details'), $details) ->addClass('ponder-question-view'); $page_objects = array_merge( array($question->getPHID()), mpull($question->getAnswers(), 'getPHID')); return $this->newPage() ->setTitle('Q'.$question->getID().' '.$question->getTitle()) ->setCrumbs($crumbs) ->setPageObjectPHIDs($page_objects) ->appendChild($ponder_view); } private function buildCurtain(PonderQuestion $question) { $viewer = $this->getViewer(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $curtain = $this->newCurtainView($question); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; } else { $name = pht('Reopen Question'); $icon = 'fa-square-o'; } $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/status/{$id}/"))); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $curtain; } private function buildSubheaderView( PonderQuestion $question) { $viewer = $this->getViewer(); $asker = $viewer->renderHandle($question->getAuthorPHID())->render(); $date = phabricator_datetime($question->getDateCreated(), $viewer); $asker = phutil_tag('strong', array(), $asker); $author = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($question->getAuthorPHID())) ->needProfileImage(true) ->executeOne(); $image_uri = $author->getProfileImageURI(); $image_href = '/p/'.$author->getUsername(); $content = pht('Asked by %s on %s.', $asker, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildPropertySectionView( PonderQuestion $question) { $viewer = $this->getViewer(); $question_details = PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer); if (!$question_details) { $question_details = phutil_tag( 'em', array(), pht('No further details for this question.')); } $question_details = phutil_tag_div( 'phabricator-remarkup ml', $question_details); return $question_details; } /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. * * TODO - re-factor this to ajax in one answer panel at a time in a more * standard fashion. This is necessary to scale this application. */ private function buildAnswers(PonderQuestion $question) { $viewer = $this->getViewer(); $answers = $question->getAnswers(); if ($answers) { $author_phids = mpull($answers, 'getAuthorPHID'); $handles = $this->loadViewerHandles($author_phids); $view = array(); foreach ($answers as $answer) { $id = $answer->getID(); $handle = $handles[$answer->getAuthorPHID()]; $timeline = $this->buildTransactionTimeline( $answer, id(new PonderAnswerTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); $xactions = $timeline->getTransactions(); $view[] = id(new PonderAnswerView()) ->setUser($viewer) ->setAnswer($answer) ->setTransactions($xactions) ->setTimeline($timeline) ->setHandle($handle); } $header = id(new PHUIHeaderView()) ->setHeader('Answers'); return id(new PHUIBoxView()) ->addClass('ponder-answer-section') ->appendChild($header) ->appendChild($view); } return null; } } diff --git a/src/applications/project/controller/PhabricatorProjectMembersAddController.php b/src/applications/project/controller/PhabricatorProjectMembersAddController.php index a3b89e46fd..79c66dc5b1 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersAddController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersAddController.php @@ -1,77 +1,77 @@ getViewer(); $id = $request->getURIData('id'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $done_uri = "/project/members/{$id}/"; if (!$project->supportsEditMembers()) { $copy = pht('Parent projects and milestones do not support adding '. 'members. You can add members directly to any non-parent subproject.'); return $this->newDialog() ->setTitle(pht('Unsupported Project')) ->appendParagraph($copy) ->addCancelButton($done_uri); } if ($request->isFormPost()) { $member_phids = $request->getArr('memberPHIDs'); $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type_member) ->setNewValue( array( '+' => array_fuse($member_phids), )); - $editor = id(new PhabricatorProjectTransactionEditor($project)) + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($done_uri); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('memberPHIDs') ->setLabel(pht('Members')) ->setDatasource(new PhabricatorPeopleDatasource())); return $this->newDialog() ->setTitle(pht('Add Members')) ->appendForm($form) ->addCancelButton($done_uri) ->addSubmitButton(pht('Add Members')); } } diff --git a/src/applications/project/controller/PhabricatorProjectMembersRemoveController.php b/src/applications/project/controller/PhabricatorProjectMembersRemoveController.php index ea41ea7113..029d4374b6 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersRemoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersRemoveController.php @@ -1,95 +1,95 @@ getViewer(); $id = $request->getURIData('id'); $type = $request->getURIData('type'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needMembers(true) ->needWatchers(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } if ($type == 'watchers') { $is_watcher = true; $edge_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; } else { if (!$project->supportsEditMembers()) { return new Aphront404Response(); } $is_watcher = false; $edge_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; } $members_uri = $this->getApplicationURI('members/'.$project->getID().'/'); $remove_phid = $request->getStr('phid'); if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue( array( '-' => array($remove_phid => $remove_phid), )); - $editor = id(new PhabricatorProjectTransactionEditor($project)) + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($members_uri); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($remove_phid)) ->executeOne(); $target_name = phutil_tag('strong', array(), $handle->getName()); $project_name = phutil_tag('strong', array(), $project->getName()); if ($is_watcher) { $title = pht('Remove Watcher'); $body = pht( 'Remove %s as a watcher of %s?', $target_name, $project_name); $button = pht('Remove Watcher'); } else { $title = pht('Remove Member'); $body = pht( 'Remove %s as a project member of %s?', $target_name, $project_name); $button = pht('Remove Member'); } return $this->newDialog() ->setTitle($title) ->addHiddenInput('phid', $remove_phid) ->appendParagraph($body) ->addCancelButton($members_uri) ->addSubmitButton($button); } } diff --git a/src/applications/project/controller/PhabricatorProjectSilenceController.php b/src/applications/project/controller/PhabricatorProjectSilenceController.php index 6edd9bab39..ead0a83da2 100644 --- a/src/applications/project/controller/PhabricatorProjectSilenceController.php +++ b/src/applications/project/controller/PhabricatorProjectSilenceController.php @@ -1,87 +1,87 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needMembers(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $edge_type = PhabricatorProjectSilencedEdgeType::EDGECONST; $done_uri = "/project/members/{$id}/"; $viewer_phid = $viewer->getPHID(); if (!$project->isUserMember($viewer_phid)) { return $this->newDialog() ->setTitle(pht('Not a Member')) ->appendParagraph( pht( 'You are not a project member, so you do not receive mail sent '. 'to members of this project.')) ->addCancelButton($done_uri); } $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( $project->getPHID(), $edge_type); $silenced = array_fuse($silenced); $is_silenced = isset($silenced[$viewer_phid]); if ($request->isDialogFormPost()) { if ($is_silenced) { $edge_action = '-'; } else { $edge_action = '+'; } $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue( array( $edge_action => array($viewer_phid => $viewer_phid), )); - $editor = id(new PhabricatorProjectTransactionEditor($project)) + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($done_uri); } if ($is_silenced) { $title = pht('Enable Mail'); $body = pht( 'When mail is sent to members of this project, you will receive a '. 'copy.'); $button = pht('Enable Project Mail'); } else { $title = pht('Disable Mail'); $body = pht( 'When mail is sent to members of this project, you will no longer '. 'receive a copy.'); $button = pht('Disable Project Mail'); } return $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($done_uri) ->addSubmitButton($button); } } diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php index 9f8c204c55..7cbd77b4cb 100644 --- a/src/applications/project/controller/PhabricatorProjectUpdateController.php +++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php @@ -1,120 +1,120 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, ); switch ($action) { case 'join': $capabilities[] = PhabricatorPolicyCapability::CAN_JOIN; break; case 'leave': break; default: return new Aphront404Response(); } $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needMembers(true) ->requireCapabilities($capabilities) ->executeOne(); if (!$project) { return new Aphront404Response(); } $done_uri = "/project/members/{$id}/"; if (!$project->supportsEditMembers()) { $copy = pht('Parent projects and milestones do not support adding '. 'members. You can add members directly to any non-parent subproject.'); return $this->newDialog() ->setTitle(pht('Unsupported Project')) ->appendParagraph($copy) ->addCancelButton($done_uri); } if ($request->isFormPost()) { $edge_action = null; switch ($action) { case 'join': $edge_action = '+'; break; case 'leave': $edge_action = '-'; break; } $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $member_spec = array( $edge_action => array($viewer->getPHID() => $viewer->getPHID()), ); $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type_member) ->setNewValue($member_spec); - $editor = id(new PhabricatorProjectTransactionEditor($project)) + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($done_uri); } $is_locked = $project->getIsMembershipLocked(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $can_leave = ($can_edit || !$is_locked); $button = null; if ($action == 'leave') { if ($can_leave) { $title = pht('Leave Project'); $body = pht( 'Your tremendous contributions to this project will be sorely '. 'missed. Are you sure you want to leave?'); $button = pht('Leave Project'); } else { $title = pht('Membership Locked'); $body = pht( 'Membership for this project is locked. You can not leave.'); } } else { $title = pht('Join Project'); $body = pht( 'Join this project? You will become a member and enjoy whatever '. 'benefits membership may confer.'); $button = pht('Join Project'); } $dialog = $this->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($done_uri); if ($button) { $dialog->addSubmitButton($button); } return $dialog; } } diff --git a/src/applications/project/controller/PhabricatorProjectWatchController.php b/src/applications/project/controller/PhabricatorProjectWatchController.php index 00117768e1..cc83f782c6 100644 --- a/src/applications/project/controller/PhabricatorProjectWatchController.php +++ b/src/applications/project/controller/PhabricatorProjectWatchController.php @@ -1,115 +1,115 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needMembers(true) ->needWatchers(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $via = $request->getStr('via'); if ($via == 'profile') { $done_uri = "/project/profile/{$id}/"; } else { $done_uri = "/project/members/{$id}/"; } $is_watcher = $project->isUserWatcher($viewer->getPHID()); $is_ancestor = $project->isUserAncestorWatcher($viewer->getPHID()); if ($is_ancestor && !$is_watcher) { $ancestor_phid = $project->getWatchedAncestorPHID($viewer->getPHID()); $handles = $viewer->loadHandles(array($ancestor_phid)); $ancestor_handle = $handles[$ancestor_phid]; return $this->newDialog() ->setTitle(pht('Watching Ancestor')) ->appendParagraph( pht( 'You are already watching %s, an ancestor of this project, and '. 'are thus watching all of its subprojects.', $ancestor_handle->renderTag()->render())) ->addCancelbutton($done_uri); } if ($request->isDialogFormPost()) { $edge_action = null; switch ($action) { case 'watch': $edge_action = '+'; break; case 'unwatch': $edge_action = '-'; break; } $type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $member_spec = array( $edge_action => array($viewer->getPHID() => $viewer->getPHID()), ); $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type_watcher) ->setNewValue($member_spec); - $editor = id(new PhabricatorProjectTransactionEditor($project)) + $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($done_uri); } $dialog = null; switch ($action) { case 'watch': $title = pht('Watch Project?'); $body = array(); $body[] = pht( 'Watching a project will let you monitor it closely. You will '. 'receive email and notifications about changes to every object '. 'tagged with projects you watch.'); $body[] = pht( 'Watching a project also watches all subprojects and milestones of '. 'that project.'); $submit = pht('Watch Project'); break; case 'unwatch': $title = pht('Unwatch Project?'); $body = pht( 'You will no longer receive email or notifications about every '. 'object associated with this project.'); $submit = pht('Unwatch Project'); break; default: return new Aphront404Response(); } $dialog = $this->newDialog() ->setTitle($title) ->addHiddenInput('via', $via) ->addCancelButton($done_uri) ->addSubmitButton($submit); foreach ((array)$body as $paragraph) { $dialog->appendParagraph($paragraph); } return $dialog; } } diff --git a/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php b/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php index 6ee623147d..864aa76a24 100644 --- a/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php +++ b/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php @@ -1,19 +1,17 @@ 'project', 'icon' => 'fa-briefcase', 'name' => pht('Project'), 'default' => true, ), array( 'key' => 'tag', 'icon' => 'fa-tags', 'name' => pht('Tag'), ), array( 'key' => 'policy', 'icon' => 'fa-lock', 'name' => pht('Policy'), ), array( 'key' => 'group', 'icon' => 'fa-users', 'name' => pht('Group'), ), array( 'key' => 'folder', 'icon' => 'fa-folder', 'name' => pht('Folder'), ), array( 'key' => 'timeline', 'icon' => 'fa-calendar', 'name' => pht('Timeline'), ), array( 'key' => 'goal', 'icon' => 'fa-flag-checkered', 'name' => pht('Goal'), ), array( 'key' => 'release', 'icon' => 'fa-truck', 'name' => pht('Release'), ), array( 'key' => 'bugs', 'icon' => 'fa-bug', 'name' => pht('Bugs'), ), array( 'key' => 'cleanup', 'icon' => 'fa-trash-o', 'name' => pht('Cleanup'), ), array( 'key' => 'umbrella', 'icon' => 'fa-umbrella', 'name' => pht('Umbrella'), ), array( 'key' => 'communication', 'icon' => 'fa-envelope', 'name' => pht('Communication'), ), array( 'key' => 'organization', 'icon' => 'fa-building', 'name' => pht('Organization'), ), array( 'key' => 'infrastructure', 'icon' => 'fa-cloud', 'name' => pht('Infrastructure'), ), array( 'key' => 'account', 'icon' => 'fa-credit-card', 'name' => pht('Account'), ), array( 'key' => 'experimental', 'icon' => 'fa-flask', 'name' => pht('Experimental'), ), array( 'key' => 'milestone', 'icon' => 'fa-map-marker', 'name' => pht('Milestone'), 'special' => self::SPECIAL_MILESTONE, ), ); } protected function newIcons() { $map = self::getIconSpecifications(); $icons = array(); foreach ($map as $spec) { $special = idx($spec, 'special'); if ($special === self::SPECIAL_MILESTONE) { continue; } $icons[] = id(new PhabricatorIconSetIcon()) ->setKey($spec['key']) ->setIsDisabled(idx($spec, 'disabled')) ->setIcon($spec['icon']) ->setLabel($spec['name']); } return $icons; } private static function getIconSpecifications() { return PhabricatorEnv::getEnvConfig('projects.icons'); } public static function getDefaultIconKey() { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'default')) { return $icon['key']; } } return null; } public static function getIconIcon($key) { $spec = self::getIconSpec($key); return idx($spec, 'icon', null); } public static function getIconName($key) { $spec = self::getIconSpec($key); return idx($spec, 'name', null); } private static function getIconSpec($key) { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'key') === $key) { return $icon; } } return array(); } public static function getMilestoneIconKey() { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'special') === self::SPECIAL_MILESTONE) { return idx($icon, 'key'); } } return null; } public static function validateConfiguration($config) { if (!is_array($config)) { throw new Exception( pht('Configuration must be a list of project icon specifications.')); } foreach ($config as $idx => $value) { if (!is_array($value)) { throw new Exception( pht( 'Value for index "%s" should be a dictionary.', $idx)); } PhutilTypeSpec::checkMap( $value, array( 'key' => 'string', 'name' => 'string', 'icon' => 'string', 'special' => 'optional string', 'disabled' => 'optional bool', 'default' => 'optional bool', )); if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) { throw new Exception( pht( 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '. 'characters long and contain only lowercase letters. For example, '. '"%s" and "%s" are reasonable keys.', 'tag', 'group')); } $special = idx($value, 'special'); $valid = array( self::SPECIAL_MILESTONE => true, ); if ($special !== null) { if (empty($valid[$special])) { throw new Exception( pht( 'Icon special attribute "%s" is not valid. Recognized special '. 'attributes are: %s.', $special, implode(', ', array_keys($valid)))); } } } $default = null; $milestone = null; $keys = array(); foreach ($config as $idx => $value) { $key = $value['key']; if (isset($keys[$key])) { throw new Exception( pht( 'Project icons must have unique keys, but two icons share the '. 'same key ("%s").', $key)); } else { $keys[$key] = true; } $is_disabled = idx($value, 'disabled'); if (idx($value, 'default')) { if ($default === null) { if ($is_disabled) { throw new Exception( pht( 'The project icon marked as the default icon ("%s") must not '. 'be disabled.', $key)); } $default = $value; } else { $original_key = $default['key']; throw new Exception( pht( 'Two different icons ("%s", "%s") are marked as the default '. 'icon. Only one icon may be marked as the default.', $key, $original_key)); } } $special = idx($value, 'special'); if ($special === self::SPECIAL_MILESTONE) { if ($milestone === null) { if ($is_disabled) { throw new Exception( pht( 'The project icon ("%s") with special attribute "%s" must '. 'not be disabled', $key, - self::SPECIAL_MIILESTONE)); + self::SPECIAL_MILESTONE)); } $milestone = $value; } else { $original_key = $milestone['key']; throw new Exception( pht( 'Two different icons ("%s", "%s") are marked with special '. 'attribute "%s". Only one icon may be marked with this '. 'attribute.', $key, $original_key, self::SPECIAL_MILESTONE)); } } } if ($default === null) { throw new Exception( pht( 'Project icons must include one icon marked as the "%s" icon, '. 'but no such icon exists.', 'default')); } if ($milestone === null) { throw new Exception( pht( 'Project icons must include one icon marked with special attribute '. '"%s", but no such icon exists.', self::SPECIAL_MILESTONE)); } } private static function getColorSpecifications() { return PhabricatorEnv::getEnvConfig('projects.colors'); } public static function getColorMap() { $specifications = self::getColorSpecifications(); return ipull($specifications, 'name', 'key'); } public static function getDefaultColorKey() { $specifications = self::getColorSpecifications(); foreach ($specifications as $specification) { if (idx($specification, 'default')) { return $specification['key']; } } return null; } private static function getAvailableColorKeys() { $list = array(); $specifications = self::getDefaultColorMap(); foreach ($specifications as $specification) { $list[] = $specification['key']; } return $list; } public static function getColorName($color_key) { $map = self::getColorMap(); return idx($map, $color_key); } public static function getDefaultColorMap() { return array( array( 'key' => PHUITagView::COLOR_RED, 'name' => pht('Red'), ), array( 'key' => PHUITagView::COLOR_ORANGE, 'name' => pht('Orange'), ), array( 'key' => PHUITagView::COLOR_YELLOW, 'name' => pht('Yellow'), ), array( 'key' => PHUITagView::COLOR_GREEN, 'name' => pht('Green'), ), array( 'key' => PHUITagView::COLOR_BLUE, 'name' => pht('Blue'), 'default' => true, ), array( 'key' => PHUITagView::COLOR_INDIGO, 'name' => pht('Indigo'), ), array( 'key' => PHUITagView::COLOR_VIOLET, 'name' => pht('Violet'), ), array( 'key' => PHUITagView::COLOR_PINK, 'name' => pht('Pink'), ), array( 'key' => PHUITagView::COLOR_GREY, 'name' => pht('Grey'), ), array( 'key' => PHUITagView::COLOR_CHECKERED, 'name' => pht('Checkered'), ), ); } public static function validateColorConfiguration($config) { if (!is_array($config)) { throw new Exception( pht('Configuration must be a list of project color specifications.')); } $available_keys = self::getAvailableColorKeys(); $available_keys = array_fuse($available_keys); foreach ($config as $idx => $value) { if (!is_array($value)) { throw new Exception( pht( 'Value for index "%s" should be a dictionary.', $idx)); } PhutilTypeSpec::checkMap( $value, array( 'key' => 'string', 'name' => 'string', 'default' => 'optional bool', )); $key = $value['key']; if (!isset($available_keys[$key])) { throw new Exception( pht( 'Color key "%s" is not a valid color key. The supported color '. 'keys are: %s.', $key, implode(', ', $available_keys))); } } $default = null; $keys = array(); foreach ($config as $idx => $value) { $key = $value['key']; if (isset($keys[$key])) { throw new Exception( pht( 'Project colors must have unique keys, but two icons share the '. 'same key ("%s").', $key)); } else { $keys[$key] = true; } if (idx($value, 'default')) { if ($default === null) { $default = $value; } else { $original_key = $default['key']; throw new Exception( pht( 'Two different colors ("%s", "%s") are marked as the default '. 'color. Only one color may be marked as the default.', $key, $original_key)); } } } if ($default === null) { throw new Exception( pht( 'Project colors must include one color marked as the "%s" color, '. 'but no such color exists.', 'default')); } } } diff --git a/src/applications/releeph/query/ReleephRequestQuery.php b/src/applications/releeph/query/ReleephRequestQuery.php index 363a3b05d6..7d4f19e624 100644 --- a/src/applications/releeph/query/ReleephRequestQuery.php +++ b/src/applications/releeph/query/ReleephRequestQuery.php @@ -1,247 +1,247 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBranchIDs(array $branch_ids) { $this->branchIDs = $branch_ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withRequestedCommitPHIDs(array $requested_commit_phids) { $this->requestedCommitPHIDs = $requested_commit_phids; return $this; } public function withRequestorPHIDs(array $phids) { $this->requestorPHIDs = $phids; return $this; } public function withSeverities(array $severities) { $this->severities = $severities; return $this; } public function withRequestedObjectPHIDs(array $phids) { $this->requestedObjectPHIDs = $phids; return $this; } protected function loadPage() { $table = new ReleephRequest(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $requests) { // Load requested objects: you must be able to see an object to see // requests for it. $object_phids = mpull($requests, 'getRequestedObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); foreach ($requests as $key => $request) { $object_phid = $request->getRequestedObjectPHID(); $object = idx($objects, $object_phid); if (!$object) { unset($requests[$key]); continue; } $request->attachRequestedObject($object); } if ($this->severities) { $severities = array_fuse($this->severities); foreach ($requests as $key => $request) { // NOTE: Facebook uses a custom field here. if (ReleephDefaultFieldSelector::isFacebook()) { $severity = $request->getDetail('severity'); } else { $severity = $request->getDetail('releeph:severity'); } if (empty($severities[$severity])) { unset($requests[$key]); } } } $branch_ids = array_unique(mpull($requests, 'getBranchID')); $branches = id(new ReleephBranchQuery()) ->withIDs($branch_ids) ->setViewer($this->getViewer()) ->execute(); $branches = mpull($branches, null, 'getID'); foreach ($requests as $key => $request) { $branch = idx($branches, $request->getBranchID()); if (!$branch) { unset($requests[$key]); continue; } $request->attachBranch($branch); } // TODO: These should be serviced by the query, but are not currently // denormalized anywhere. For now, filter them here instead. Note that // we must perform this filtering *after* querying and attaching branches, // because request status depends on the product. $keep_status = array_fuse($this->getKeepStatusConstants()); if ($keep_status) { foreach ($requests as $key => $request) { if (empty($keep_status[$request->getStatus()])) { unset($requests[$key]); } } } return $requests; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->branchIDs !== null) { $where[] = qsprintf( $conn_r, 'branchID IN (%Ld)', $this->branchIDs); } if ($this->requestedCommitPHIDs !== null) { $where[] = qsprintf( $conn_r, 'requestCommitPHID IN (%Ls)', $this->requestedCommitPHIDs); } if ($this->requestorPHIDs !== null) { $where[] = qsprintf( $conn_r, 'requestUserPHID IN (%Ls)', $this->requestorPHIDs); } if ($this->requestedObjectPHIDs !== null) { $where[] = qsprintf( $conn_r, 'requestedObjectPHID IN (%Ls)', $this->requestedObjectPHIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function getKeepStatusConstants() { switch ($this->status) { case self::STATUS_ALL: return array(); case self::STATUS_OPEN: return array( ReleephRequestStatus::STATUS_REQUESTED, ReleephRequestStatus::STATUS_NEEDS_PICK, ReleephRequestStatus::STATUS_NEEDS_REVERT, ); case self::STATUS_REQUESTED: return array( ReleephRequestStatus::STATUS_REQUESTED, ); case self::STATUS_NEEDS_PULL: return array( ReleephRequestStatus::STATUS_NEEDS_PICK, ); case self::STATUS_REJECTED: return array( ReleephRequestStatus::STATUS_REJECTED, ); case self::STATUS_ABANDONED: return array( ReleephRequestStatus::STATUS_ABANDONED, ); case self::STATUS_PULLED: return array( ReleephRequestStatus::STATUS_PICKED, ); case self::STATUS_NEEDS_REVERT: return array( - ReleephRequestStatus::NEEDS_REVERT, + ReleephRequestStatus::STATUS_NEEDS_REVERT, ); case self::STATUS_REVERTED: return array( - ReleephRequestStatus::REVERTED, + ReleephRequestStatus::STATUS_REVERTED, ); default: throw new Exception(pht("Unknown status '%s'!", $this->status)); } } public function getQueryApplicationClass() { return 'PhabricatorReleephApplication'; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryURIQuery.php b/src/applications/repository/query/PhabricatorRepositoryURIQuery.php index 330fe769f5..71252a6fb7 100644 --- a/src/applications/repository/query/PhabricatorRepositoryURIQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryURIQuery.php @@ -1,106 +1,101 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withRepositoryPHIDs(array $phids) { $this->repositoryPHIDs = $phids; return $this; } public function withRepositories(array $repositories) { $repositories = mpull($repositories, null, 'getPHID'); $this->withRepositoryPHIDs(array_keys($repositories)); $this->repositories = $repositories; return $this; } - public function withObjectHashes(array $hashes) { - $this->objectHashes = $hashes; - return $this; - } - public function newResultObject() { return new PhabricatorRepositoryURI(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } return $where; } protected function willFilterPage(array $uris) { $repositories = $this->repositories; $repository_phids = mpull($uris, 'getRepositoryPHID'); $repository_phids = array_fuse($repository_phids); $repository_phids = array_diff_key($repository_phids, $repositories); if ($repository_phids) { $more_repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($repository_phids) ->execute(); $repositories += mpull($more_repositories, null, 'getPHID'); } foreach ($uris as $key => $uri) { $repository_phid = $uri->getRepositoryPHID(); $repository = idx($repositories, $repository_phid); if (!$repository) { $this->didRejectResult($uri); unset($uris[$key]); continue; } $uri->attachRepository($repository); } return $uris; } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php index 1cb3182c16..cbd25b74eb 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryURI.php +++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php @@ -1,748 +1,748 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'uri' => 'text255', 'builtinProtocol' => 'text32?', 'builtinIdentifier' => 'text32?', 'credentialPHID' => 'phid?', 'ioType' => 'text32', 'displayType' => 'text32', 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_builtin' => array( 'columns' => array( 'repositoryPHID', 'builtinProtocol', 'builtinIdentifier', ), 'unique' => true, ), ), ) + parent::getConfiguration(); } public static function initializeNewURI() { return id(new self()) ->setIoType(self::IO_DEFAULT) ->setDisplayType(self::DISPLAY_DEFAULT) ->setIsDisabled(0); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryURIPHIDType::TYPECONST); } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function getRepositoryURIBuiltinKey() { if (!$this->getBuiltinProtocol()) { return null; } $parts = array( $this->getBuiltinProtocol(), $this->getBuiltinIdentifier(), ); return implode('.', $parts); } public function isBuiltin() { return (bool)$this->getBuiltinProtocol(); } public function getEffectiveDisplayType() { $display = $this->getDisplayType(); if ($display != self::DISPLAY_DEFAULT) { return $display; } return $this->getDefaultDisplayType(); } public function getDefaultDisplayType() { switch ($this->getEffectiveIOType()) { case self::IO_MIRROR: case self::IO_OBSERVE: case self::IO_NONE: return self::DISPLAY_NEVER; case self::IO_READ: case self::IO_READWRITE: // By default, only show the "best" version of the builtin URI, not the // other redundant versions. $repository = $this->getRepository(); $other_uris = $repository->getURIs(); $identifier_value = array( self::BUILTIN_IDENTIFIER_SHORTNAME => 3, self::BUILTIN_IDENTIFIER_CALLSIGN => 2, self::BUILTIN_IDENTIFIER_ID => 1, ); $have_identifiers = array(); foreach ($other_uris as $other_uri) { if ($other_uri->getIsDisabled()) { continue; } $identifier = $other_uri->getBuiltinIdentifier(); if (!$identifier) { continue; } $have_identifiers[$identifier] = $identifier_value[$identifier]; } $best_identifier = max($have_identifiers); $this_identifier = $identifier_value[$this->getBuiltinIdentifier()]; if ($this_identifier < $best_identifier) { return self::DISPLAY_NEVER; } return self::DISPLAY_ALWAYS; } return self::DISPLAY_NEVER; } public function getEffectiveIOType() { $io = $this->getIoType(); if ($io != self::IO_DEFAULT) { return $io; } return $this->getDefaultIOType(); } public function getDefaultIOType() { if ($this->isBuiltin()) { $repository = $this->getRepository(); $other_uris = $repository->getURIs(); $any_observe = false; foreach ($other_uris as $other_uri) { if ($other_uri->getIoType() == self::IO_OBSERVE) { $any_observe = true; break; } } if ($any_observe) { return self::IO_READ; } else { return self::IO_READWRITE; } } return self::IO_NONE; } public function getNormalizedURI() { $vcs = $this->getRepository()->getVersionControlSystem(); $map = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => PhabricatorRepositoryURINormalizer::TYPE_GIT, PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => PhabricatorRepositoryURINormalizer::TYPE_SVN, PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL, ); $type = $map[$vcs]; $display = (string)$this->getDisplayURI(); $normal_uri = new PhabricatorRepositoryURINormalizer($type, $display); return $normal_uri->getNormalizedURI(); } public function getDisplayURI() { return $this->getURIObject(); } public function getEffectiveURI() { return $this->getURIObject(); } public function getURIEnvelope() { $uri = $this->getEffectiveURI(); $command_engine = $this->newCommandEngine(); $is_http = $command_engine->isAnyHTTPProtocol(); // For SVN, we use `--username` and `--password` flags separately in the // CommandEngine, so we don't need to add any credentials here. $is_svn = $this->getRepository()->isSVN(); $credential_phid = $this->getCredentialPHID(); if ($is_http && !$is_svn && $credential_phid) { $key = PassphrasePasswordKey::loadFromPHID( $credential_phid, PhabricatorUser::getOmnipotentUser()); $uri->setUser($key->getUsernameEnvelope()->openEnvelope()); $uri->setPass($key->getPasswordEnvelope()->openEnvelope()); } return new PhutilOpaqueEnvelope((string)$uri); } private function getURIObject() { // Users can provide Git/SCP-style URIs in the form "user@host:path". // In the general case, these are not equivalent to any "ssh://..." form // because the path is relative. if ($this->isBuiltin()) { $builtin_protocol = $this->getForcedProtocol(); $builtin_domain = $this->getForcedHost(); $raw_uri = "{$builtin_protocol}://{$builtin_domain}"; } else { $raw_uri = $this->getURI(); } $port = $this->getForcedPort(); $default_ports = array( 'ssh' => 22, 'http' => 80, 'https' => 443, ); $uri = new PhutilURI($raw_uri); // Make sure to remove any password from the URI before we do anything // with it; this should always be provided by the associated credential. $uri->setPass(null); $protocol = $this->getForcedProtocol(); if ($protocol) { $uri->setProtocol($protocol); } if ($port) { $uri->setPort($port); } // Remove any explicitly set default ports. $uri_port = $uri->getPort(); $uri_protocol = $uri->getProtocol(); $uri_default = idx($default_ports, $uri_protocol); if ($uri_default && ($uri_default == $uri_port)) { $uri->setPort(null); } $user = $this->getForcedUser(); if ($user) { $uri->setUser($user); } $host = $this->getForcedHost(); if ($host) { $uri->setDomain($host); } $path = $this->getForcedPath(); if ($path) { $uri->setPath($path); } return $uri; } private function getForcedProtocol() { $repository = $this->getRepository(); switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: if ($repository->isSVN()) { return 'svn+ssh'; } else { return 'ssh'; } case self::BUILTIN_PROTOCOL_HTTP: return 'http'; case self::BUILTIN_PROTOCOL_HTTPS: return 'https'; default: return null; } } private function getForcedUser() { switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: return AlmanacKeys::getClusterSSHUser(); default: return null; } } private function getForcedHost() { $phabricator_uri = PhabricatorEnv::getURI('/'); $phabricator_uri = new PhutilURI($phabricator_uri); $phabricator_host = $phabricator_uri->getDomain(); switch ($this->getBuiltinProtocol()) { case self::BUILTIN_PROTOCOL_SSH: $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); if ($ssh_host !== null) { return $ssh_host; } return $phabricator_host; case self::BUILTIN_PROTOCOL_HTTP: case self::BUILTIN_PROTOCOL_HTTPS: return $phabricator_host; default: return null; } } private function getForcedPort() { $protocol = $this->getBuiltinProtocol(); if ($protocol == self::BUILTIN_PROTOCOL_SSH) { return PhabricatorEnv::getEnvConfig('diffusion.ssh-port'); } // If Phabricator is running on a nonstandard port, use that as the defualt // port for URIs with the same protocol. $is_http = ($protocol == self::BUILTIN_PROTOCOL_HTTP); $is_https = ($protocol == self::BUILTIN_PROTOCOL_HTTPS); if ($is_http || $is_https) { $uri = PhabricatorEnv::getURI('/'); $uri = new PhutilURI($uri); $port = $uri->getPort(); if (!$port) { return null; } $uri_protocol = $uri->getProtocol(); $use_port = ($is_http && ($uri_protocol == 'http')) || ($is_https && ($uri_protocol == 'https')); if (!$use_port) { return null; } return $port; } return null; } private function getForcedPath() { if (!$this->isBuiltin()) { return null; } $repository = $this->getRepository(); $id = $repository->getID(); $callsign = $repository->getCallsign(); $short_name = $repository->getRepositorySlug(); $clone_name = $repository->getCloneName(); if ($repository->isGit()) { $suffix = '.git'; } else if ($repository->isHg()) { $suffix = '/'; } else { $suffix = ''; $clone_name = ''; } switch ($this->getBuiltinIdentifier()) { case self::BUILTIN_IDENTIFIER_ID: return "/diffusion/{$id}/{$clone_name}{$suffix}"; case self::BUILTIN_IDENTIFIER_SHORTNAME: return "/source/{$short_name}{$suffix}"; case self::BUILTIN_IDENTIFIER_CALLSIGN: return "/diffusion/{$callsign}/{$clone_name}{$suffix}"; default: return null; } } public function getViewURI() { $id = $this->getID(); return $this->getRepository()->getPathURI("uri/view/{$id}/"); } public function getEditURI() { $id = $this->getID(); return $this->getRepository()->getPathURI("uri/edit/{$id}/"); } public function getAvailableIOTypeOptions() { $options = array( self::IO_DEFAULT, self::IO_NONE, ); if ($this->isBuiltin()) { $options[] = self::IO_READ; $options[] = self::IO_READWRITE; } else { $options[] = self::IO_OBSERVE; $options[] = self::IO_MIRROR; } $map = array(); $io_map = self::getIOTypeMap(); foreach ($options as $option) { $spec = idx($io_map, $option, array()); $label = idx($spec, 'label', $option); $short = idx($spec, 'short'); $name = pht('%s: %s', $label, $short); $map[$option] = $name; } return $map; } public function getAvailableDisplayTypeOptions() { $options = array( self::DISPLAY_DEFAULT, self::DISPLAY_ALWAYS, self::DISPLAY_NEVER, ); $map = array(); $display_map = self::getDisplayTypeMap(); foreach ($options as $option) { $spec = idx($display_map, $option, array()); $label = idx($spec, 'label', $option); $short = idx($spec, 'short'); $name = pht('%s: %s', $label, $short); $map[$option] = $name; } return $map; } public static function getIOTypeMap() { return array( self::IO_DEFAULT => array( 'label' => pht('Default'), 'short' => pht('Use default behavior.'), ), self::IO_OBSERVE => array( 'icon' => 'fa-download', 'color' => 'green', 'label' => pht('Observe'), 'note' => pht( 'Phabricator will observe changes to this URI and copy them.'), 'short' => pht('Copy from a remote.'), ), self::IO_MIRROR => array( 'icon' => 'fa-upload', 'color' => 'green', 'label' => pht('Mirror'), 'note' => pht( 'Phabricator will push a copy of any changes to this URI.'), 'short' => pht('Push a copy to a remote.'), ), self::IO_NONE => array( 'icon' => 'fa-times', 'color' => 'grey', 'label' => pht('No I/O'), 'note' => pht( 'Phabricator will not push or pull any changes to this URI.'), 'short' => pht('Do not perform any I/O.'), ), self::IO_READ => array( 'icon' => 'fa-folder', 'color' => 'blue', 'label' => pht('Read Only'), 'note' => pht( 'Phabricator will serve a read-only copy of the repository from '. 'this URI.'), 'short' => pht('Serve repository in read-only mode.'), ), self::IO_READWRITE => array( 'icon' => 'fa-folder-open', 'color' => 'blue', 'label' => pht('Read/Write'), 'note' => pht( 'Phabricator will serve a read/write copy of the repository from '. 'this URI.'), 'short' => pht('Serve repository in read/write mode.'), ), ); } public static function getDisplayTypeMap() { return array( self::DISPLAY_DEFAULT => array( 'label' => pht('Default'), 'short' => pht('Use default behavior.'), ), self::DISPLAY_ALWAYS => array( 'icon' => 'fa-eye', 'color' => 'green', 'label' => pht('Visible'), 'note' => pht('This URI will be shown to users as a clone URI.'), 'short' => pht('Show as a clone URI.'), ), self::DISPLAY_NEVER => array( 'icon' => 'fa-eye-slash', 'color' => 'grey', 'label' => pht('Hidden'), 'note' => pht( 'This URI will be hidden from users.'), 'short' => pht('Do not show as a clone URI.'), ), ); } public function newCommandEngine() { $repository = $this->getRepository(); return DiffusionCommandEngine::newCommandEngine($repository) ->setCredentialPHID($this->getCredentialPHID()) ->setURI($this->getEffectiveURI()); } public function getURIScore() { $score = 0; $io_points = array( self::IO_READWRITE => 200, self::IO_READ => 100, ); - $score += idx($io_points, $this->getEffectiveIoType(), 0); + $score += idx($io_points, $this->getEffectiveIOType(), 0); $protocol_points = array( self::BUILTIN_PROTOCOL_SSH => 30, self::BUILTIN_PROTOCOL_HTTPS => 20, self::BUILTIN_PROTOCOL_HTTP => 10, ); $score += idx($protocol_points, $this->getBuiltinProtocol(), 0); $identifier_points = array( self::BUILTIN_IDENTIFIER_SHORTNAME => 3, self::BUILTIN_IDENTIFIER_CALLSIGN => 2, self::BUILTIN_IDENTIFIER_ID => 1, ); $score += idx($identifier_points, $this->getBuiltinIdentifier(), 0); return $score; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DiffusionURIEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryURITransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: // To edit a repository URI, you must be able to edit the // corresponding repository. $extended[] = array($this->getRepository(), $capability); break; } return $extended; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('The associated repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('uri') ->setType('map') ->setDescription(pht('The raw and effective URI.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('io') ->setType('map') ->setDescription( pht('The raw, default, and effective I/O Type settings.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('display') ->setType('map') ->setDescription( pht('The raw, default, and effective Display Type settings.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('credentialPHID') ->setType('phid?') ->setDescription( pht('The associated credential PHID, if one exists.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('disabled') ->setType('bool') ->setDescription(pht('True if the URI is disabled.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('builtin') ->setType('map') ->setDescription( pht('Information about builtin URIs.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dateCreated') ->setType('int') ->setDescription( pht('Epoch timestamp when the object was created.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dateModified') ->setType('int') ->setDescription( pht('Epoch timestamp when the object was last updated.')), ); } public function getFieldValuesForConduit() { return array( 'repositoryPHID' => $this->getRepositoryPHID(), 'uri' => array( 'raw' => $this->getURI(), 'display' => (string)$this->getDisplayURI(), 'effective' => (string)$this->getEffectiveURI(), 'normalized' => (string)$this->getNormalizedURI(), ), 'io' => array( 'raw' => $this->getIOType(), 'default' => $this->getDefaultIOType(), 'effective' => $this->getEffectiveIOType(), ), 'display' => array( 'raw' => $this->getDisplayType(), 'default' => $this->getDefaultDisplayType(), 'effective' => $this->getEffectiveDisplayType(), ), 'credentialPHID' => $this->getCredentialPHID(), 'disabled' => (bool)$this->getIsDisabled(), 'builtin' => array( 'protocol' => $this->getBuiltinProtocol(), 'identifier' => $this->getBuiltinIdentifier(), ), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index d5ca8b7a5e..4329509b02 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1403 +1,1403 @@ controller = $controller; return $this; } public function getController() { return $this->controller; } public function buildResponse() { $controller = $this->getController(); $request = $controller->getRequest(); $search = id(new PhabricatorApplicationSearchController()) ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine($this); return $controller->delegateToController($search); } 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 setNavigationItems(array $navigation_items) { assert_instances_of($navigation_items, 'PHUIListItemView'); $this->navigationItems = $navigation_items; return $this; } public function getNavigationItems() { return $this->navigationItems; } 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. + * @return PhabricatorQuery The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) { $saved = clone $original; $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; } $original->attachParameterMap($map); $query = $this->buildQueryFromParameters($map); $object = $this->newResultObject(); if (!$object) { return $query; } $extensions = $this->getEngineExtensions(); foreach ($extensions as $extension) { $extension->applyConstraintsToQuery($object, $query, $saved, $map); } $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) { $extensions = $this->getEngineExtensions(); foreach ($extensions as $extension) { $extension_fields = $extension->getSearchFields($object); foreach ($extension_fields as $extension_field) { $fields[] = $extension_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); } $buckets = $this->newResultBuckets(); if ($query && $buckets) { $bucket_options = array( self::BUCKET_NONE => pht('No Bucketing'), ) + mpull($buckets, 'getResultBucketName'); $fields[] = id(new PhabricatorSearchSelectField()) ->setLabel(pht('Bucket')) ->setKey('bucket') ->setOptions($bucket_options); } $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 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 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/'); } public function getQueryBaseURI() { return $this->getURI(''); } /** * 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'); foreach ($this->navigationItems as $extra_item) { $menu->addMenuItem($extra_item); } return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); - $builtin = $this->getBuiltinQueries($viewer); + $builtin = $this->getBuiltinQueries(); if ($this->namedQueries === null) { $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 = 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'); $this->namedQueries = $named_queries; } return $this->namedQueries + $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 All available engines. * @task construct */ public static function getAllEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->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 Other permitted PHID types. * @return list 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 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 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 Optional, list of permitted PHID types. * @return list 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 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 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 )--------------------------------------- */ protected function newResultBuckets() { return array(); } public function getResultBucket(PhabricatorSavedQuery $saved) { $key = $saved->getParameter('bucket'); if ($key == self::BUCKET_NONE) { return null; } $buckets = $this->newResultBuckets(); return idx($buckets, $key); } public function getPageSize(PhabricatorSavedQuery $saved) { $bucket = $this->getResultBucket($saved); $limit = (int)$saved->getParameter('limit'); if ($limit > 0) { if ($bucket) { $bucket->setPageSize($limit); } return $limit; } if ($bucket) { return $bucket->getPageSize(); } 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(); } abstract protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles); /* -( Application Search )------------------------------------------------- */ public function getSearchFieldsForConduit() { $standard_fields = $this->buildSearchFields(); $fields = array(); foreach ($standard_fields as $field_key => $field) { $conduit_key = $field->getConduitKey(); if (isset($fields[$conduit_key])) { $other = $fields[$conduit_key]; $other_key = $other->getKey(); throw new Exception( pht( 'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '. 'define the same Conduit key ("%s"). Keys must be unique.', $field_key, get_class($field), $other_key, get_class($other), $conduit_key)); } $fields[$conduit_key] = $field; } // These are handled separately for Conduit, so don't show them as // supported. unset($fields['order']); unset($fields['limit']); $viewer = $this->requireViewer(); foreach ($fields as $key => $field) { $field->setViewer($viewer); } return $fields; } public function buildConduitResponse( ConduitAPIRequest $request, ConduitAPIMethod $method) { $viewer = $this->requireViewer(); $query_key = $request->getValue('queryKey'); if (!strlen($query_key)) { $saved_query = new PhabricatorSavedQuery(); } else if ($this->isBuiltinQuery($query_key)) { $saved_query = $this->buildSavedQueryFromBuiltin($query_key); } else { $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved_query) { throw new Exception( pht( 'Query key "%s" does not correspond to a valid query.', $query_key)); } } $constraints = $request->getValue('constraints', array()); $fields = $this->getSearchFieldsForConduit(); foreach ($fields as $key => $field) { if (!$field->getConduitParameterType()) { unset($fields[$key]); } } $valid_constraints = array(); foreach ($fields as $field) { foreach ($field->getValidConstraintKeys() as $key) { $valid_constraints[$key] = true; } } foreach ($constraints as $key => $constraint) { if (empty($valid_constraints[$key])) { throw new Exception( pht( 'Constraint "%s" is not a valid constraint for this query.', $key)); } } foreach ($fields as $field) { if (!$field->getValueExistsInConduitRequest($constraints)) { continue; } $value = $field->readValueFromConduitRequest( $constraints, $request->getIsStrictlyTyped()); $saved_query->setParameter($field->getKey(), $value); } // NOTE: Currently, when running an ad-hoc query we never persist it into // a saved query. We might want to add an option to do this in the future // (for example, to enable a CLI-to-Web workflow where user can view more // details about results by following a link), but have no use cases for // it today. If we do identify a use case, we could save the query here. $query = $this->buildQueryFromSavedQuery($saved_query); $pager = $this->newPagerForSavedQuery($saved_query); $attachments = $this->getConduitSearchAttachments(); // TODO: Validate this better. $attachment_specs = $request->getValue('attachments', array()); $attachments = array_select_keys( $attachments, array_keys($attachment_specs)); foreach ($attachments as $key => $attachment) { $attachment->setViewer($viewer); } foreach ($attachments as $key => $attachment) { $attachment->willLoadAttachmentData($query, $attachment_specs[$key]); } $this->setQueryOrderForConduit($query, $request); $this->setPagerLimitForConduit($pager, $request); $this->setPagerOffsetsForConduit($pager, $request); $objects = $this->executeQuery($query, $pager); $data = array(); if ($objects) { $field_extensions = $this->getConduitFieldExtensions(); $extension_data = array(); foreach ($field_extensions as $key => $extension) { $extension_data[$key] = $extension->loadExtensionConduitData($objects); } $attachment_data = array(); foreach ($attachments as $key => $attachment) { $attachment_data[$key] = $attachment->loadAttachmentData( $objects, $attachment_specs[$key]); } foreach ($objects as $object) { $field_map = $this->getObjectWireFieldsForConduit( $object, $field_extensions, $extension_data); $attachment_map = array(); foreach ($attachments as $key => $attachment) { $attachment_map[$key] = $attachment->getAttachmentForObject( $object, $attachment_data[$key], $attachment_specs[$key]); } // If this is empty, we still want to emit a JSON object, not a // JSON list. if (!$attachment_map) { $attachment_map = (object)$attachment_map; } $id = (int)$object->getID(); $phid = $object->getPHID(); $data[] = array( 'id' => $id, 'type' => phid_get_type($phid), 'phid' => $phid, 'fields' => $field_map, 'attachments' => $attachment_map, ); } } return array( 'data' => $data, 'maps' => $method->getQueryMaps($query), 'query' => array( // This may be `null` if we have not saved the query. 'queryKey' => $saved_query->getQueryKey(), ), 'cursor' => array( 'limit' => $pager->getPageSize(), 'after' => $pager->getNextPageID(), 'before' => $pager->getPrevPageID(), 'order' => $request->getValue('order'), ), ); } public function getAllConduitFieldSpecifications() { $extensions = $this->getConduitFieldExtensions(); $object = $this->newQuery()->newResultObject(); $map = array(); foreach ($extensions as $extension) { $specifications = $extension->getFieldSpecificationsForConduit($object); foreach ($specifications as $specification) { $key = $specification->getKey(); if (isset($map[$key])) { throw new Exception( pht( 'Two field specifications share the same key ("%s"). Each '. 'specification must have a unique key.', $key)); } $map[$key] = $specification; } } return $map; } private function getEngineExtensions() { $extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions(); foreach ($extensions as $key => $extension) { $extension ->setViewer($this->requireViewer()) ->setSearchEngine($this); } $object = $this->newResultObject(); foreach ($extensions as $key => $extension) { if (!$extension->supportsObject($object)) { unset($extensions[$key]); } } return $extensions; } private function getConduitFieldExtensions() { $extensions = $this->getEngineExtensions(); $object = $this->newResultObject(); foreach ($extensions as $key => $extension) { if (!$extension->getFieldSpecificationsForConduit($object)) { unset($extensions[$key]); } } return $extensions; } private function setQueryOrderForConduit($query, ConduitAPIRequest $request) { $order = $request->getValue('order'); if ($order === null) { return; } if (is_scalar($order)) { $query->setOrder($order); } else { $query->setOrderVector($order); } } private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) { $limit = $request->getValue('limit'); // If there's no limit specified and the query uses a weird huge page // size, just leave it at the default gigantic page size. Otherwise, // make sure it's between 1 and 100, inclusive. if ($limit === null) { if ($pager->getPageSize() >= 0xFFFF) { return; } else { $limit = 100; } } if ($limit > 100) { throw new Exception( pht( 'Maximum page size for Conduit API method calls is 100, but '. 'this call specified %s.', $limit)); } if ($limit < 1) { throw new Exception( pht( 'Minimum page size for API searches is 1, but this call '. 'specified %s.', $limit)); } $pager->setPageSize($limit); } private function setPagerOffsetsForConduit( $pager, ConduitAPIRequest $request) { $before_id = $request->getValue('before'); if ($before_id !== null) { $pager->setBeforeID($before_id); } $after_id = $request->getValue('after'); if ($after_id !== null) { $pager->setAfterID($after_id); } } protected function getObjectWireFieldsForConduit( $object, array $field_extensions, array $extension_data) { $fields = array(); foreach ($field_extensions as $key => $extension) { $data = idx($extension_data, $key, array()); $fields += $extension->getFieldValuesForConduit($object, $data); } return $fields; } public function getConduitSearchAttachments() { $extensions = $this->getEngineExtensions(); $object = $this->newResultObject(); $attachments = array(); foreach ($extensions as $extension) { $extension_attachments = $extension->getSearchAttachments($object); foreach ($extension_attachments as $attachment) { $attachment_key = $attachment->getAttachmentKey(); if (isset($attachments[$attachment_key])) { $other = $attachments[$attachment_key]; throw new Exception( pht( 'Two search engine attachments (of classes "%s" and "%s") '. 'specify the same attachment key ("%s"); keys must be unique.', get_class($attachment), get_class($other), $attachment_key)); } $attachments[$attachment_key] = $attachment; } } return $attachments; } final public function renderNewUserView() { $body = $this->getNewUserBody(); if (!$body) { return null; } return $body; } protected function getNewUserHeader() { return null; } protected function getNewUserBody() { return null; } public function newUseResultsActions(PhabricatorSavedQuery $saved) { return array(); } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index f41424a4e7..4b24e67c6b 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -1,1318 +1,1318 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->profileObject; } public function setCustomPHID($custom_phid) { $this->customPHID = $custom_phid; return $this; } public function getCustomPHID() { return $this->customPHID; } private function getEditModeCustomPHID() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: $custom_phid = $this->getCustomPHID(); break; case self::MODE_GLOBAL: $custom_phid = null; break; } return $custom_phid; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } private function setDefaultItem( PhabricatorProfileMenuItemConfiguration $default_item) { $this->defaultItem = $default_item; return $this; } public function getDefaultItem() { $this->getItems(); return $this->defaultItem; } public function setShowNavigation($show) { $this->showNavigation = $show; return $this; } public function getShowNavigation() { return $this->showNavigation; } public function addContentPageClass($class) { $this->pageClasses[] = $class; return $this; } public function setShowContentCrumbs($show_content_crumbs) { $this->showContentCrumbs = $show_content_crumbs; return $this; } public function getShowContentCrumbs() { return $this->showContentCrumbs; } abstract public function getItemURI($path); abstract protected function isMenuEngineConfigurable(); abstract protected function getBuiltinProfileItems($object); protected function getBuiltinCustomProfileItems( $object, $custom_phid) { return array(); } protected function getEditMode() { return $this->editMode; } public function buildResponse() { $controller = $this->getController(); $viewer = $controller->getViewer(); $this->setViewer($viewer); $request = $controller->getRequest(); $item_action = $request->getURIData('itemAction'); if (!$item_action) { $item_action = 'view'; } $is_view = ($item_action == 'view'); // If the engine is not configurable, don't respond to any of the editing // or configuration routes. if (!$this->isMenuEngineConfigurable()) { if (!$is_view) { return new Aphront404Response(); } } $item_id = $request->getURIData('itemID'); // If we miss on the MenuEngine route, try the EditEngine route. This will // be populated while editing items. if (!$item_id) { $item_id = $request->getURIData('id'); } $item_list = $this->getItems(); $selected_item = null; if (strlen($item_id)) { $item_id_int = (int)$item_id; foreach ($item_list as $item) { if ($item_id_int) { if ((int)$item->getID() === $item_id_int) { $selected_item = $item; break; } } $builtin_key = $item->getBuiltinKey(); if ($builtin_key === (string)$item_id) { $selected_item = $item; break; } } } if (!$selected_item) { if ($is_view) { $selected_item = $this->getDefaultItem(); } } switch ($item_action) { case 'view': case 'info': case 'hide': case 'default': case 'builtin': if (!$selected_item) { return new Aphront404Response(); } break; case 'edit': if (!$request->getURIData('id')) { // If we continue along the "edit" pathway without an ID, we hit an // unrelated exception because we can not build a new menu item out // of thin air. For menus, new items are created via the "new" // action. Just catch this case and 404 early since there's currently // no clean way to make EditEngine aware of this. return new Aphront404Response(); } break; } $navigation = $this->buildNavigation(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if (!$is_view) { $navigation->selectFilter(self::ITEM_MANAGE); if ($selected_item) { if ($selected_item->getCustomPHID()) { $edit_mode = 'custom'; } else { $edit_mode = 'global'; } } else { $edit_mode = $request->getURIData('itemEditMode'); } - $available_modes = $this->getViewerEditModes($viewer); + $available_modes = $this->getViewerEditModes(); if ($available_modes) { $available_modes = array_fuse($available_modes); if (isset($available_modes[$edit_mode])) { $this->editMode = $edit_mode; } else { if ($item_action != 'configure') { return new Aphront404Response(); } } } $page_title = pht('Configure Menu'); } else { $page_title = $selected_item->getDisplayName(); } switch ($item_action) { case 'view': $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); $content = $this->buildItemViewContent($selected_item); $crumbs->addTextCrumb($selected_item->getDisplayName()); if (!$content) { return new Aphront404Response(); } break; case 'configure': $mode = $this->getEditMode(); if (!$mode) { $crumbs->addTextCrumb(pht('Configure Menu')); $content = $this->buildMenuEditModeContent(); } else { if (count($available_modes) > 1) { $crumbs->addTextCrumb( pht('Configure Menu'), $this->getItemURI('configure/')); switch ($mode) { case self::MODE_CUSTOM: $crumbs->addTextCrumb(pht('Personal')); break; case self::MODE_GLOBAL: $crumbs->addTextCrumb(pht('Global')); break; } } else { $crumbs->addTextCrumb(pht('Configure Menu')); } $edit_list = $this->loadItems($mode); $content = $this->buildItemConfigureContent($edit_list); } break; case 'reorder': $mode = $this->getEditMode(); $edit_list = $this->loadItems($mode); $content = $this->buildItemReorderContent($edit_list); break; case 'new': $item_key = $request->getURIData('itemKey'); $mode = $this->getEditMode(); $content = $this->buildItemNewContent($item_key, $mode); break; case 'builtin': $content = $this->buildItemBuiltinContent($selected_item); break; case 'hide': $content = $this->buildItemHideContent($selected_item); break; case 'default': if (!$this->isMenuEnginePinnable()) { return new Aphront404Response(); } $content = $this->buildItemDefaultContent( $selected_item, $item_list); break; case 'edit': $content = $this->buildItemEditContent(); break; default: throw new Exception( pht( 'Unsupported item action "%s".', $item_action)); } if ($content instanceof AphrontResponse) { return $content; } if ($content instanceof AphrontResponseProducerInterface) { return $content; } $crumbs->setBorder(true); $page = $controller->newPage() ->setTitle($page_title) ->appendChild($content); if (!$is_view || $this->getShowContentCrumbs()) { $page->setCrumbs($crumbs); } if ($this->getShowNavigation()) { $page->setNavigation($navigation); } if ($is_view) { foreach ($this->pageClasses as $class) { $page->addClass($class); } } return $page; } public function buildNavigation() { if ($this->navigation) { return $this->navigation; } $nav = id(new AphrontSideNavFilterView()) ->setIsProfileMenu(true) ->setBaseURI(new PhutilURI($this->getItemURI(''))); $menu_items = $this->getItems(); $filtered_items = array(); foreach ($menu_items as $menu_item) { if ($menu_item->isDisabled()) { continue; } $filtered_items[] = $menu_item; } $filtered_groups = mgroup($filtered_items, 'getMenuItemKey'); foreach ($filtered_groups as $group) { $first_item = head($group); $first_item->willBuildNavigationItems($group); } foreach ($menu_items as $menu_item) { if ($menu_item->isDisabled()) { continue; } $items = $menu_item->buildNavigationMenuItems(); foreach ($items as $item) { $this->validateNavigationMenuItem($item); } // If the item produced only a single item which does not otherwise // have a key, try to automatically assign it a reasonable key. This // makes selecting the correct item simpler. if (count($items) == 1) { $item = head($items); if ($item->getKey() === null) { $default_key = $menu_item->getDefaultMenuItemKey(); $item->setKey($default_key); } } foreach ($items as $item) { $nav->addMenuItem($item); } } $nav->selectFilter(null); $this->navigation = $nav; return $this->navigation; } private function getItems() { if ($this->items === null) { $this->items = $this->loadItems(self::MODE_COMBINED); } return $this->items; } private function loadItems($mode) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $items = $this->loadBuiltinProfileItems($mode); $query = id(new PhabricatorProfileMenuItemConfigurationQuery()) ->setViewer($viewer) ->withProfilePHIDs(array($object->getPHID())); switch ($mode) { case self::MODE_GLOBAL: $query->withCustomPHIDs(array(), true); break; case self::MODE_CUSTOM: $query->withCustomPHIDs(array($this->getCustomPHID()), false); break; case self::MODE_COMBINED: $query->withCustomPHIDs(array($this->getCustomPHID()), true); break; } $stored_items = $query->execute(); foreach ($stored_items as $stored_item) { $impl = $stored_item->getMenuItem(); $impl->setViewer($viewer); $impl->setEngine($this); } // Merge the stored items into the builtin items. If a builtin item has // a stored version, replace the defaults with the stored changes. foreach ($stored_items as $stored_item) { if (!$stored_item->shouldEnableForObject($object)) { continue; } $builtin_key = $stored_item->getBuiltinKey(); if ($builtin_key !== null) { // If this builtin actually exists, replace the builtin with the // stored configuration. Otherwise, we're just going to drop the // stored config: it corresponds to an out-of-date or uninstalled // item. if (isset($items[$builtin_key])) { $items[$builtin_key] = $stored_item; } else { continue; } } else { $items[] = $stored_item; } } $items = $this->arrangeItems($items, $mode); // Make sure exactly one valid item is marked as default. $default = null; $first = null; foreach ($items as $item) { if (!$item->canMakeDefault()) { continue; } // If this engine doesn't support pinning items, don't respect any // setting which might be present in the database. if ($this->isMenuEnginePinnable()) { if ($item->isDefault()) { $default = $item; break; } } if ($first === null) { $first = $item; } } if (!$default) { $default = $first; } if ($default) { $this->setDefaultItem($default); } return $items; } private function loadBuiltinProfileItems($mode) { $object = $this->getProfileObject(); switch ($mode) { case self::MODE_GLOBAL: $builtins = $this->getBuiltinProfileItems($object); break; case self::MODE_CUSTOM: $builtins = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); break; case self::MODE_COMBINED: $builtins = array(); $builtins[] = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); $builtins[] = $this->getBuiltinProfileItems($object); $builtins = array_mergev($builtins); break; } $items = PhabricatorProfileMenuItem::getAllMenuItems(); $viewer = $this->getViewer(); $order = 1; $map = array(); foreach ($builtins as $builtin) { $builtin_key = $builtin->getBuiltinKey(); if (!$builtin_key) { throw new Exception( pht( 'Object produced a builtin item with no builtin item key! '. 'Builtin items must have a unique key.')); } if (isset($map[$builtin_key])) { throw new Exception( pht( 'Object produced two items with the same builtin key ("%s"). '. 'Each item must have a unique builtin key.', $builtin_key)); } $item_key = $builtin->getMenuItemKey(); $item = idx($items, $item_key); if (!$item) { throw new Exception( pht( 'Builtin item ("%s") specifies a bad item key ("%s"); there '. 'is no corresponding item implementation available.', $builtin_key, $item_key)); } $item = clone $item; $item->setViewer($viewer); $item->setEngine($this); $builtin ->setProfilePHID($object->getPHID()) ->attachMenuItem($item) ->attachProfileObject($object) ->setMenuItemOrder($order); if (!$builtin->shouldEnableForObject($object)) { continue; } $map[$builtin_key] = $builtin; $order++; } return $map; } private function validateNavigationMenuItem($item) { if (!($item instanceof PHUIListItemView)) { throw new Exception( pht( 'Expected buildNavigationMenuItems() to return a list of '. 'PHUIListItemView objects, but got a surprise.')); } } public function getConfigureURI() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: return $this->getItemURI('configure/custom/'); case self::MODE_GLOBAL: return $this->getItemURI('configure/global/'); } return $this->getItemURI('configure/'); } private function buildItemReorderContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); // If you're reordering global items, you need to be able to edit the // object the menu appears on. If you're reordering custom items, you only // need to be able to edit the custom object. Currently, the custom object // is always the viewing user's own user object. $custom_phid = $this->getEditModeCustomPHID(); if (!$custom_phid) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } else { $policy_object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($custom_phid)) ->executeOne(); if (!$policy_object) { throw new Exception( pht( 'Failed to load custom PHID "%s"!', $custom_phid)); } PhabricatorPolicyFilter::requireCapability( $viewer, $policy_object, PhabricatorPolicyCapability::CAN_EDIT); } $controller = $this->getController(); $request = $controller->getRequest(); $request->validateCSRF(); $order = $request->getStrList('order'); $by_builtin = array(); $by_id = array(); foreach ($items as $key => $item) { $id = $item->getID(); if ($id) { $by_id[$id] = $key; continue; } $builtin_key = $item->getBuiltinKey(); if ($builtin_key) { $by_builtin[$builtin_key] = $key; continue; } } $key_order = array(); foreach ($order as $order_item) { if (isset($by_id[$order_item])) { $key_order[] = $by_id[$order_item]; continue; } if (isset($by_builtin[$order_item])) { $key_order[] = $by_builtin[$order_item]; continue; } } $items = array_select_keys($items, $key_order) + $items; $type_order = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER; $order = 1; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($order); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); $order++; } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } protected function buildItemViewContent( PhabricatorProfileMenuItemConfiguration $item) { return $item->newPageContent(); } private function getViewerEditModes() { $modes = array(); $viewer = $this->getViewer(); if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) { $modes[] = self::MODE_CUSTOM; } $object = $this->getProfileObject(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); if ($can_edit) { $modes[] = self::MODE_GLOBAL; } return $modes; } protected function isMenuEnginePersonalizable() { return true; } /** * Does this engine support pinning items? * * Personalizable menus disable pinning by default since it creates a number * of weird edge cases without providing many benefits for current menus. * * @return bool True if items may be pinned as default items. */ protected function isMenuEnginePinnable() { return !$this->isMenuEnginePersonalizable(); } private function buildMenuEditModeContent() { $viewer = $this->getViewer(); - $modes = $this->getViewerEditModes($viewer); + $modes = $this->getViewerEditModes(); if (!$modes) { return new Aphront404Response(); } if (count($modes) == 1) { $mode = head($modes); return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("configure/{$mode}/")); } $menu = id(new PHUIObjectItemListView()) ->setUser($viewer); $modes = array_fuse($modes); if (isset($modes['custom'])) { $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Personal Menu Items')) ->setHref($this->getItemURI('configure/custom/')) ->setImageURI($viewer->getProfileImageURI()) ->addAttribute(pht('Edit the menu for your personal account.'))); } if (isset($modes['global'])) { $icon = id(new PHUIIconView()) ->setIcon('fa-globe') ->setBackground('bg-blue'); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Global Menu Items')) ->setHref($this->getItemURI('configure/global/')) ->setImageIcon($icon) ->addAttribute(pht('Edit the global default menu for all users.'))); } $box = id(new PHUIObjectBoxView()) ->setObjectList($menu); $header = id(new PHUIHeaderView()) ->setHeader(pht('Manage Menu')) ->setHeaderIcon('fa-list'); return id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); } private function buildItemConfigureContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $filtered_groups = mgroup($items, 'getMenuItemKey'); foreach ($filtered_groups as $group) { $first_item = head($group); $first_item->willBuildNavigationItems($group); } // Users only need to be able to edit the object which this menu appears // on if they're editing global menu items. For example, users do not need // to be able to edit the Favorites application to add new items to the // Favorites menu. if (!$this->getCustomPHID()) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } $list_id = celerity_generate_unique_node_id(); $mode = $this->getEditMode(); Javelin::initBehavior( 'reorder-profile-menu-items', array( 'listID' => $list_id, 'orderURI' => $this->getItemURI("reorder/{$mode}/"), )); $list = id(new PHUIObjectItemListView()) ->setID($list_id) ->setNoDataString(pht('This menu currently has no items.')); foreach ($items as $item) { $id = $item->getID(); $builtin_key = $item->getBuiltinKey(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $item, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PHUIObjectItemView()); $name = $item->getDisplayName(); $type = $item->getMenuItemTypeName(); if (!strlen(trim($name))) { $name = pht('Untitled "%s" Item', $type); } $view->setHeader($name); $view->addAttribute($type); if ($can_edit) { $view ->setGrippable(true) ->addSigil('profile-menu-item') ->setMetadata( array( 'key' => nonempty($id, $builtin_key), )); if ($id) { $default_uri = $this->getItemURI("default/{$id}/"); } else { $default_uri = $this->getItemURI("default/{$builtin_key}/"); } $default_text = null; if ($this->isMenuEnginePinnable()) { if ($item->isDefault()) { $default_icon = 'fa-thumb-tack green'; $default_text = pht('Current Default'); } else if ($item->canMakeDefault()) { $default_icon = 'fa-thumb-tack'; $default_text = pht('Make Default'); } } if ($default_text !== null) { $view->addAction( id(new PHUIListItemView()) ->setHref($default_uri) ->setWorkflow(true) ->setName($default_text) ->setIcon($default_icon)); } if ($id) { $view->setHref($this->getItemURI("edit/{$id}/")); $hide_uri = $this->getItemURI("hide/{$id}/"); } else { $view->setHref($this->getItemURI("builtin/{$builtin_key}/")); $hide_uri = $this->getItemURI("hide/{$builtin_key}/"); } if ($item->isDisabled()) { $hide_icon = 'fa-plus'; $hide_text = pht('Enable'); } else if ($item->getBuiltinKey() !== null) { $hide_icon = 'fa-times'; $hide_text = pht('Disable'); } else { $hide_icon = 'fa-times'; $hide_text = pht('Delete'); } $can_disable = $item->canHideMenuItem(); $view->addAction( id(new PHUIListItemView()) ->setHref($hide_uri) ->setWorkflow(true) ->setDisabled(!$can_disable) ->setName($hide_text) ->setIcon($hide_icon)); } if ($item->isDisabled()) { $view->setDisabled(true); } $list->addItem($view); } $action_view = id(new PhabricatorActionListView()) ->setUser($viewer); $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $object = $this->getProfileObject(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Add New Menu Item...'))); foreach ($item_types as $item_type) { if (!$item_type->canAddToObject($object)) { continue; } $item_key = $item_type->getMenuItemKey(); $edit_mode = $this->getEditMode(); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon($item_type->getMenuItemTypeIcon()) ->setName($item_type->getMenuItemTypeName()) ->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/")) ->setWorkflow(true)); } $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Documentation'))); $doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide'); $doc_name = pht('Profile Menu User Guide'); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-book') ->setHref($doc_link) ->setName($doc_name)); $header = id(new PHUIHeaderView()) ->setHeader(pht('Menu Items')) ->setHeaderIcon('fa-list'); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Menu Items')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); $panel = id(new PHUICurtainPanelView()) ->appendChild($action_view); $curtain = id(new PHUICurtainView()) ->setViewer($viewer) ->setActionList($action_list); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $box, )); return $view; } private function buildItemNewContent($item_key, $mode) { $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $item_type = idx($item_types, $item_key); if (!$item_type) { return new Aphront404Response(); } $object = $this->getProfileObject(); if (!$item_type->canAddToObject($object)) { return new Aphront404Response(); } $custom_phid = $this->getEditModeCustomPHID(); $configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem( $object, $item_type, $custom_phid); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $controller = $this->getController(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setCustomPHID($custom_phid) ->setController($controller) ->buildResponse(); } private function buildItemEditContent() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemBuiltinContent( PhabricatorProfileMenuItemConfiguration $configuration) { // If this builtin item has already been persisted, redirect to the // edit page. $id = $configuration->getID(); if ($id) { return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("edit/{$id}/")); } // Otherwise, act like we're creating a new item, we're just starting // with the builtin template. $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setIsBuiltin(true) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemHideContent( PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); if (!$configuration->canHideMenuItem()) { return $controller->newDialog() ->setTitle(pht('Mandatory Item')) ->appendParagraph( pht('This menu item is very important, and can not be disabled.')) ->addCancelButton($this->getConfigureURI()); } if ($configuration->getBuiltinKey() === null) { $new_value = null; $title = pht('Delete Menu Item'); $body = pht('Delete this menu item?'); $button = pht('Delete Menu Item'); } else if ($configuration->isDisabled()) { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $title = pht('Enable Menu Item'); $body = pht( 'Enable this menu item? It will appear in the menu again.'); $button = pht('Enable Menu Item'); } else { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED; $title = pht('Disable Menu Item'); $body = pht( 'Disable this menu item? It will no longer appear in the menu, but '. 'you can re-enable it later.'); $button = pht('Disable Menu Item'); } $v_visibility = $configuration->getVisibility(); if ($request->isFormPost()) { if ($new_value === null) { $configuration->delete(); } else { $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($new_value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($configuration, $xactions); } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } return $controller->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($this->getConfigureURI()) ->addSubmitButton($button); } private function buildItemDefaultContent( PhabricatorProfileMenuItemConfiguration $configuration, array $items) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $done_uri = $this->getConfigureURI(); if (!$configuration->canMakeDefault()) { return $controller->newDialog() ->setTitle(pht('Not Defaultable')) ->appendParagraph( pht( 'This item can not be set as the default item. This is usually '. 'because the item has no page of its own, or links to an '. 'external page.')) ->addCancelButton($done_uri); } if ($configuration->isDefault()) { return $controller->newDialog() ->setTitle(pht('Already Default')) ->appendParagraph( pht( 'This item is already set as the default item for this menu.')) ->addCancelButton($done_uri); } if ($request->isFormPost()) { $key = $configuration->getID(); if (!$key) { $key = $configuration->getBuiltinKey(); } $this->adjustDefault($key); return id(new AphrontRedirectResponse()) ->setURI($done_uri); } return $controller->newDialog() ->setTitle(pht('Make Default')) ->appendParagraph( pht( 'Set this item as the default for this menu? Users arriving on '. 'this page will be shown the content of this item by default.')) ->addCancelButton($done_uri) ->addSubmitButton(pht('Make Default')); } protected function newItem() { return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin(); } protected function newManageItem() { return $this->newItem() ->setBuiltinKey(self::ITEM_MANAGE) ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY); } public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $items = $this->loadItems(self::MODE_COMBINED); // To adjust the default item, we first change any existing items that // are marked as defaults to "visible", then make the new default item // the default. $default = array(); $visible = array(); foreach ($items as $item) { $builtin_key = $item->getBuiltinKey(); $id = $item->getID(); $is_target = (($builtin_key !== null) && ($builtin_key === $key)) || (($id !== null) && ((int)$id === (int)$key)); if ($is_target) { if (!$item->isDefault()) { $default[] = $item; } } else { if ($item->isDefault()) { $visible[] = $item; } } } $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT; $apply = array( array($v_visible, $visible), array($v_default, $default), ); foreach ($apply as $group) { list($value, $items) = $group; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); } } return $this; } private function arrangeItems(array $items, $mode) { // Sort the items. $items = msortv($items, 'getSortVector'); $object = $this->getProfileObject(); // If we have some global items and some custom items and are in "combined" // mode, put a hard-coded divider item between them. if ($mode == self::MODE_COMBINED) { $list = array(); $seen_custom = false; $seen_global = false; foreach ($items as $item) { if ($item->getCustomPHID()) { $seen_custom = true; } else { if ($seen_custom && !$seen_global) { $list[] = $this->newItem() ->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER) ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) ->attachProfileObject($object) ->attachMenuItem( new PhabricatorDividerProfileMenuItem()); } $seen_global = true; } $list[] = $item; } $items = $list; } // Normalize keys since callers shouldn't rely on this array being // partially keyed. $items = array_values($items); return $items; } } diff --git a/src/applications/search/storage/PhabricatorSavedQuery.php b/src/applications/search/storage/PhabricatorSavedQuery.php index 5bc8a95915..3f75027888 100644 --- a/src/applications/search/storage/PhabricatorSavedQuery.php +++ b/src/applications/search/storage/PhabricatorSavedQuery.php @@ -1,84 +1,84 @@ array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineClassName' => 'text255', 'queryKey' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_queryKey' => array( 'columns' => array('queryKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function setParameter($key, $value) { $this->parameters[$key] = $value; return $this; } public function getParameter($key, $default = null) { return idx($this->parameters, $key, $default); } public function save() { if ($this->getEngineClassName() === null) { throw new Exception(pht('Engine class is null.')); } // Instantiate the engine to make sure it's valid. $this->newEngine(); $serial = $this->getEngineClassName().serialize($this->parameters); $this->queryKey = PhabricatorHash::digestForIndex($serial); return parent::save(); } public function newEngine() { return newv($this->getEngineClassName(), array()); } public function attachParameterMap(array $map) { $this->parameterMap = $map; return $this; } - public function getEvaluatedParameter($key, $default = null) { - return $this->assertAttachedKey($this->parameterMap, $key, $default); + public function getEvaluatedParameter($key) { + return $this->assertAttachedKey($this->parameterMap, $key); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_PUBLIC; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php index 2d630bffc1..c69a825c3a 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php @@ -1,103 +1,103 @@ getViewer(); $id = $request->getURIData('id'); $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needOptions(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } if ($poll->getIsClosed()) { return new Aphront400Response(); } $options = $poll->getOptions(); $viewer_choices = $poll->getViewerChoices($viewer); $old_votes = mpull($viewer_choices, null, 'getOptionID'); if ($request->isAjax()) { $vote = $request->getInt('vote'); $votes = array_keys($old_votes); - $votes = array_fuse($votes, $votes); + $votes = array_fuse($votes); if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { if (idx($votes, $vote, false)) { $votes = array(); } else { $votes = array($vote); } } else { if (idx($votes, $vote, false)) { unset($votes[$vote]); } else { $votes[$vote] = $vote; } } $this->updateVotes($viewer, $poll, $old_votes, $votes); $updated_choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d AND authorPHID = %s', $poll->getID(), $viewer->getPHID()); $embed = id(new SlowvoteEmbedView()) ->setPoll($poll) ->setOptions($options) ->setViewerChoices($updated_choices); return id(new AphrontAjaxResponse()) ->setContent(array( 'pollID' => $poll->getID(), 'contentHTML' => $embed->render(), )); } if (!$request->isFormPost()) { return id(new Aphront404Response()); } $votes = $request->getArr('vote'); - $votes = array_fuse($votes, $votes); + $votes = array_fuse($votes); $this->updateVotes($viewer, $poll, $old_votes, $votes); return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID()); } private function updateVotes($viewer, $poll, $old_votes, $votes) { if (!empty($votes) && count($votes) > 1 && $poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { return id(new Aphront400Response()); } foreach ($old_votes as $old_vote) { if (!idx($votes, $old_vote->getOptionID(), false)) { $old_vote->delete(); } } foreach ($votes as $vote) { if (idx($old_votes, $vote, false)) { continue; } id(new PhabricatorSlowvoteChoice()) ->setAuthorPHID($viewer->getPHID()) ->setPollID($poll->getID()) ->setOptionID($vote) ->save(); } } } diff --git a/src/applications/spaces/controller/PhabricatorSpacesListController.php b/src/applications/spaces/controller/PhabricatorSpacesListController.php index fe439b0097..c270171574 100644 --- a/src/applications/spaces/controller/PhabricatorSpacesListController.php +++ b/src/applications/spaces/controller/PhabricatorSpacesListController.php @@ -1,52 +1,52 @@ getRequest(); - $controller = id(new PhabricatorApplicationSearchController($request)) + $controller = id(new PhabricatorApplicationSearchController()) ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine(new PhabricatorSpacesNamespaceSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView($for_app = false) { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorSpacesNamespaceSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $can_create = $this->hasApplicationCapability( PhabricatorSpacesCapabilityCreateSpaces::CAPABILITY); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Space')) ->setHref($this->getApplicationURI('create/')) ->setIcon('fa-plus-square') ->setDisabled(!$can_create) ->setWorkflow(!$can_create)); return $crumbs; } } diff --git a/src/applications/tokens/storage/PhabricatorTokensToken.php b/src/applications/tokens/storage/PhabricatorTokensToken.php index 8cd83cd884..50bada38b9 100644 --- a/src/applications/tokens/storage/PhabricatorTokensToken.php +++ b/src/applications/tokens/storage/PhabricatorTokensToken.php @@ -1,157 +1,158 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', 'flavor' => 'text128', 'status' => 'text32', 'tokenImagePHID' => 'phid?', 'builtinKey' => 'text32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_creator' => array( 'columns' => array('creatorPHID', 'dateModified'), ), 'key_builtin' => array( 'columns' => array('builtinKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getTableName() { return 'token_token'; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorTokenTokenPHIDType::TYPECONST); } public static function initializeNewToken(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorTokensApplication')) ->executeOne(); $token = id(new self()) ->setCreatorPHID($actor->getPHID()) ->setStatus(self::STATUS_ACTIVE) ->setTokenImagePHID(''); return $token; } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } public function getTokenImageURI() { return $this->getTokenImageFile()->getBestURI(); } public function attachTokenImageFile(PhabricatorFile $file) { $this->tokenImageFile = $file; return $this; } public function getTokenImageFile() { return $this->assertAttached($this->tokenImageFile); } public function getViewURI() { return '/tokens/view/'.$this->getID().'/'; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $tokens = id(new PhabricatorTokenGiven()) ->loadAllWhere('tokenPHID = %s', $this->getPHID()); foreach ($tokens as $token) { $token->delete(); } if ($this->getTokenImagePHID()) { id(new PhabricatorFile()) ->loadOneWhere('filePHID = %s', $this->getTokenImagePHID()) ->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the token.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('flavor') ->setType('string') ->setDescription(pht('Token flavor.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Archived or active status.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'flavor' => $this->getFlavor(), 'status' => $this->getStatus(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index f69a2cd1bd..996b12bc51 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,494 +1,495 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setCurrentVersion($current_version) { $this->currentVersion = $current_version; return $this; } public function getCurrentVersion() { return $this->currentVersion; } public function setVersionedDraft( PhabricatorVersionedDraft $versioned_draft) { $this->versionedDraft = $versioned_draft; return $this; } public function getVersionedDraft() { return $this->versionedDraft; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function setFullWidth($fw) { $this->fullWidth = $fw; return $this; } public function setInfoView(PHUIInfoView $info_view) { $this->infoView = $info_view; return $this; } public function getInfoView() { return $this->infoView; } public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; return $this; } public function getCommentActions() { return $this->commentActions; } public function setCommentActionGroups(array $groups) { assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup'); $this->commentActionGroups = $groups; return $this; } public function getCommentActionGroups() { return $this->commentActionGroups; } public function setNoPermission($no_permission) { $this->noPermission = $no_permission; return $this; } public function getNoPermission() { return $this->noPermission; } public function setTransactionTimeline( PhabricatorApplicationTransactionView $timeline) { $timeline->setQuoteTargetID($this->getCommentID()); if ($this->getNoPermission()) { $timeline->setShouldTerminate(true); } $this->transactionTimeline = $timeline; return $this; } public function render() { if ($this->getNoPermission()) { return null; } $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string)$this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', 'href' => $uri, ), pht('Login to Comment'))); } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } if (!$this->getCommentActions()) { Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), )); } require_celerity_resource('phui-comment-form-css'); $image_uri = $user->getProfileImageURI(); $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-comment-image', )); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge', ), ''); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->addClass('phui-comment-form-view') ->addSigil('phui-comment-form') ->appendChild($image) ->appendChild($wedge) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $draft_comment = ''; $draft_key = null; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); $draft_key = $this->getDraft()->getDraftKey(); } $versioned_draft = $this->getVersionedDraft(); if ($versioned_draft) { $draft_comment = $versioned_draft->getProperty('comment', ''); } if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID', 'render'); } $version_key = PhabricatorVersionedDraft::KEY_VERSION; $version_value = $this->getCurrentVersion(); $form = id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->addHiddenInput('__draft__', $draft_key) ->addHiddenInput($version_key, $version_value); $comment_actions = $this->getCommentActions(); if ($comment_actions) { $action_map = array(); $type_map = array(); $comment_actions = mpull($comment_actions, null, 'getKey'); $draft_actions = array(); $draft_keys = array(); if ($versioned_draft) { $draft_actions = $versioned_draft->getProperty('actions', array()); if (!is_array($draft_actions)) { $draft_actions = array(); } foreach ($draft_actions as $action) { $type = idx($action, 'type'); $comment_action = idx($comment_actions, $type); if (!$comment_action) { continue; } $value = idx($action, 'value'); $comment_action->setValue($value); $draft_keys[] = $type; } } foreach ($comment_actions as $key => $comment_action) { $key = $comment_action->getKey(); $action_map[$key] = array( 'key' => $key, 'label' => $comment_action->getLabel(), 'type' => $comment_action->getPHUIXControlType(), 'spec' => $comment_action->getPHUIXControlSpecification(), 'initialValue' => $comment_action->getInitialValue(), 'groupKey' => $comment_action->getGroupKey(), 'conflictKey' => $comment_action->getConflictKey(), ); $type_map[$key] = $comment_action; } $options = $this->newCommentActionOptions($action_map); $action_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $place_id = celerity_generate_unique_node_id(); $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'editengine.actions', 'id' => $input_id, ))); $invisi_bar = phutil_tag( 'div', array( 'id' => $place_id, 'class' => 'phui-comment-control-stack', )); $action_select = id(new AphrontFormSelectControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-action-control') ->setID($action_id) ->setOptions($options); $action_bar = phutil_tag( 'div', array( 'class' => 'phui-comment-action-bar grouped', ), array( $action_select, )); $form->appendChild($action_bar); $info_view = $this->getInfoView(); if ($info_view) { $form->appendChild($info_view); } $form->appendChild($invisi_bar); $form->addClass('phui-comment-has-actions'); Javelin::initBehavior( 'comment-actions', array( 'actionID' => $action_id, 'inputID' => $input_id, 'formID' => $this->getFormID(), 'placeID' => $place_id, 'panelID' => $this->getPreviewPanelID(), 'timelineID' => $this->getPreviewTimelineID(), 'actions' => $action_map, 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), 'drafts' => $draft_keys, )); } $submit_button = id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName()); $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-textarea-control') ->setCanPin(true) ->setName('comment') ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName())); return $form; } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', 'class' => 'phui-comment-preview-view', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } private function newCommentActionOptions(array $action_map) { $options = array(); $options['+'] = pht('Add Action...'); // Merge options into groups. $groups = array(); foreach ($action_map as $key => $item) { $group_key = $item['groupKey']; if (!isset($groups[$group_key])) { $groups[$group_key] = array(); } $groups[$group_key][$key] = $item; } $group_specs = $this->getCommentActionGroups(); $group_labels = mpull($group_specs, 'getLabel', 'getKey'); // Reorder groups to put them in the same order as the recognized // group definitions. $groups = array_select_keys($groups, array_keys($group_labels)) + $groups; // Move options with no group to the end. $default_group = idx($groups, ''); if ($default_group) { unset($groups['']); $groups[''] = $default_group; } foreach ($groups as $group_key => $group_items) { if (strlen($group_key)) { $group_label = idx($group_labels, $group_key, $group_key); $options[$group_label] = ipull($group_items, 'label'); } else { foreach ($group_items as $key => $item) { $options[$key] = $item['label']; } } } return $options; } } diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php index 52707b1a03..95955c1c2a 100644 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php @@ -1,170 +1,170 @@ server); return $uri->getDomain(); } public function connect() { $this->server = $this->getConfig('server'); $this->authtoken = $this->getConfig('authtoken'); $rooms = $this->getConfig('join'); // First, join the room if (!$rooms) { throw new Exception(pht('Not configured to join any rooms!')); } $this->readBuffers = array(); // Set up our long poll in a curl multi request so we can // continue running while it executes in the background $this->multiHandle = curl_multi_init(); $this->readHandles = array(); foreach ($rooms as $room_id) { $this->joinRoom($room_id); // Set up the curl stream for reading $url = $this->buildStreamingUrl($room_id); $ch = $this->readHandles[$url] = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt( $ch, CURLOPT_USERPWD, $this->authtoken.':x'); curl_setopt( $ch, CURLOPT_HTTPHEADER, array('Content-type: application/json')); curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array($this, 'read')); curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); curl_setopt($ch, CURLOPT_TIMEOUT, 0); curl_multi_add_handle($this->multiHandle, $ch); // Initialize read buffer $this->readBuffers[$url] = ''; } $this->active = null; $this->blockingMultiExec(); } protected function joinRoom($room_id) { // Optional hook, by default, do nothing } // This is our callback for the background curl multi-request. // Puts the data read in on the readBuffer for processing. private function read($ch, $data) { $info = curl_getinfo($ch); $length = strlen($data); $this->readBuffers[$info['url']] .= $data; return $length; } private function blockingMultiExec() { do { $status = curl_multi_exec($this->multiHandle, $this->active); } while ($status == CURLM_CALL_MULTI_PERFORM); // Check for errors if ($status != CURLM_OK) { throw new Exception( pht('Phabricator Bot had a problem reading from stream.')); } } public function getNextMessages($poll_frequency) { $messages = array(); if (!$this->active) { throw new Exception(pht('Phabricator Bot stopped reading from stream.')); } // Prod our http request curl_multi_select($this->multiHandle, $poll_frequency); $this->blockingMultiExec(); // Process anything waiting on the read buffer while ($m = $this->processReadBuffer()) { $messages[] = $m; } return $messages; } private function processReadBuffer() { foreach ($this->readBuffers as $url => &$buffer) { $until = strpos($buffer, "}\r"); if ($until == false) { continue; } $message = substr($buffer, 0, $until + 1); $buffer = substr($buffer, $until + 2); $m_obj = phutil_json_decode($message); if ($message = $this->processMessage($m_obj)) { return $message; } } // If we're here, there's nothing to process return false; } protected function performPost($endpoint, $data = null) { $uri = new PhutilURI($this->server); $uri->setPath($endpoint); $payload = json_encode($data); list($output) = id(new HTTPSFuture($uri)) ->setMethod('POST') ->addHeader('Content-Type', 'application/json') ->addHeader('Authorization', $this->getAuthorizationHeader()) ->setData($payload) ->resolvex(); $output = trim($output); if (strlen($output)) { return phutil_json_decode($output); } return true; } protected function getAuthorizationHeader() { return 'Basic '.$this->getEncodedAuthToken(); } protected function getEncodedAuthToken() { return base64_encode($this->authtoken.':x'); } abstract protected function buildStreamingUrl($channel); abstract protected function processMessage(array $raw_object); } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index 0527c3cf1e..c6712ae873 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,2021 +1,2021 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or `null` if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing */ abstract class LiskDAO extends Phobject { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_BINARY = 'binary'; const CONFIG_COLUMN_SCHEMA = 'col-schema'; const CONFIG_KEY_SCHEMA = 'key-schema'; const CONFIG_NO_TABLE = 'no-table'; const CONFIG_NO_MUTATE = 'no-mutate'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $ephemeral = false; private $forcedConnection; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ abstract protected function getConnectionNamespace(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. - * @return AprontDatabaseConnection|null Connection, if it exists in cache. + * @return AphrontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /** * Force an object to use a specific connection. * * This overrides all connection management and forces the object to use * a specific connection when interacting with the database. * * @param AphrontDatabaseConnection Connection to force this object to use. * @task conn */ public function setForcedConnection(AphrontDatabaseConnection $connection) { $this->forcedConnection = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * protected function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_BINARY * You can optionally provide a map of columns to a flag indicating that * they store binary data. These columns will not raise an error when * handling binary writes. * * CONFIG_COLUMN_SCHEMA * Provide a map of columns to schema column types. * * CONFIG_KEY_SCHEMA * Provide a map of key names to key specifications. * * CONFIG_NO_TABLE * Allows you to specify that this object does not actually have a table in * the database. * * CONFIG_NO_MUTATE * Provide a map of columns which should not be included in UPDATE statements. * If you have some columns which are always written to explicitly and should * never be overwritten by a save(), you can specify them here. This is an * advanced, specialized feature and there are usually better approaches for * most locking/contention problems. * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontCountQueryException( pht( 'More than one result from %s!', __FUNCTION__.'()')); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($pattern /* , $args... */) { $connection = $this->establishConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 1); $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception( pht("Unable to reload object that hasn't been loaded!")); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { throw new AphrontObjectMissingQueryException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via @{method:queryData}) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling * @{method:getProperties}. * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with @{method:loadAllWhere}, but sometimes that * isn't flexible enough. One case is if you need to do joins to select the * right objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than @{method:loadAllWhere}, but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorCalendarEvent(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { throw new AphrontCountQueryException( pht( 'More than one result from %s!', __FUNCTION__.'()')); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } public function getPHID() { return $this->phid; } /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getAllLiskProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getAllLiskProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. - * @return LiskDatabaseConnection Lisk connection object. + * @return AphrontDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception( pht( "Unknown mode '%s', should be 'r' or 'w'.", $mode)); } if ($this->forcedConnection) { return $this->forcedConnection; } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using @{method:loadFromArray} (unless you're * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you * should just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getAllLiskPropertyValues() { $map = array(); foreach ($this->getAllLiskProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); // Remove colums flagged as nonmutable from the update statement. $no_mutate = $this->getConfigOption(self::CONFIG_NO_MUTATE); if ($no_mutate) { foreach ($no_mutate as $column) { unset($data[$column]); } } $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); $binary = $this->getBinaryColumns(); foreach ($map as $key => $value) { if (!empty($binary[$key])) { $map[$key] = qsprintf($conn, '%C = %nB', $key, $value); } else { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } } $map = implode(', ', $map); $id = $this->getID(); $conn->query( 'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this->getTableName(), $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getAllLiskPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterValue($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception(pht('Unknown %s mechanism!', 'CONFIG_IDs')); } $this->willWriteData($data); $columns = array_keys($data); $binary = $this->getBinaryColumns(); foreach ($data as $key => $value) { try { if (!empty($binary[$key])) { $data[$key] = qsprintf($conn, '%nB', $value); } else { $data[$key] = qsprintf($conn, '%ns', $value); } } catch (AphrontParameterQueryException $parameter_exception) { throw new PhutilProxyException( pht( "Unable to insert or update object of class %s, field '%s' ". "has a non-scalar value.", get_class($this), $key), $parameter_exception); } } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( pht( 'You are using manual IDs. You must override the %s method '. 'to properly detect when to insert a new record.', __FUNCTION__.'()')); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( pht( 'This DAO does not have a single-part primary key. The method you '. 'called requires a single-part primary key.')); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ public function generatePHID() { $type = $this->getPHIDType(); return PhabricatorPHID::generateNewPHID($type); } public function getPHIDType() { throw new PhutilMethodNotImplementedException(); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also @{method:willReadData}. * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also @{method:willWriteData}. * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior * of @{method:getField} instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( pht('Lisk process isolation level was reduced below 0.')); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( pht('Lisk transaction isolation level was reduced below 0.')); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } /** * Close any connections with no recent activity. * * Long-running processes can use this method to clean up connections which * have not been used recently. * * @param int Close connections with no activity for this many seconds. * @return void */ public static function closeInactiveConnections($idle_window) { $connections = self::$connections; $now = PhabricatorTime::getNow(); foreach ($connections as $key => $connection) { $last_active = $connection->getLastActiveEpoch(); $idle_duration = ($now - $last_active); if ($idle_duration <= $idle_window) { continue; } self::closeConnection($key); } } public static function closeAllConnections() { $connections = self::$connections; foreach ($connections as $key => $connection) { self::closeConnection($key); } } private static function closeConnection($key) { if (empty(self::$connections[$key])) { throw new Exception( pht( 'No database connection with connection key "%s" exists!', $key)); } $connection = self::$connections[$key]; unset(self::$connections[$key]); $connection->close(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = phutil_json_encode($data[$col]); } break; default: throw new Exception( pht("Unknown serialization format '%s'.", $format)); } } } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception(pht("Unable to resolve method '%s'!", $method)); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception(pht('Bad getter call: %s', $method)); } $dispatch_map[$method] = $property; } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception(pht("Unable to resolve method '%s'!", $method)); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception(pht('Bad setter call: %s', $method)); } $dispatch_map[$method] = $property; } $this->writeField($property, $args[0]); return $this; } throw new Exception(pht("Unable to resolve method '%s'.", $method)); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { // Hack for policy system hints, see PhabricatorPolicyRule for notes. if ($name != '_hashKey') { phlog( pht( 'Wrote to undeclared property %s.', get_class($this).'::$'.$name)); } $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterValue( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } /** * Returns the current value of a named counter. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to read. * @return int|null Current value, or `null` if the counter does not exist. * * @task util */ public static function loadCurrentCounterValue( AphrontDatabaseConnection $conn_r, $counter_name) { $row = queryfx_one( $conn_r, 'SELECT counterValue FROM %T WHERE counterName = %s', self::COUNTER_TABLE_NAME, $counter_name); if (!$row) { return null; } return (int)$row['counterValue']; } /** * Overwrite a named counter, forcing it to a specific value. * * If the counter does not exist, it is created. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or overwrite. * @return void * * @task util */ public static function overwriteCounterValue( AphrontDatabaseConnection $conn_w, $counter_name, $counter_value) { queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, %d) ON DUPLICATE KEY UPDATE counterValue = VALUES(counterValue)', self::COUNTER_TABLE_NAME, $counter_name, $counter_value); } private function getBinaryColumns() { return $this->getConfigOption(self::CONFIG_BINARY); } public function getSchemaColumns() { $custom_map = $this->getConfigOption(self::CONFIG_COLUMN_SCHEMA); if (!$custom_map) { $custom_map = array(); } $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if (!$serialization) { $serialization = array(); } $serialization_map = array( self::SERIALIZATION_JSON => 'text', self::SERIALIZATION_PHP => 'bytes', ); $binary_map = $this->getBinaryColumns(); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); if ($id_mechanism == self::IDS_AUTOINCREMENT) { $id_type = 'auto'; } else { $id_type = 'id'; } $builtin = array( 'id' => $id_type, 'phid' => 'phid', 'viewPolicy' => 'policy', 'editPolicy' => 'policy', 'epoch' => 'epoch', 'dateCreated' => 'epoch', 'dateModified' => 'epoch', ); $map = array(); foreach ($this->getAllLiskProperties() as $property) { // First, use types specified explicitly in the table configuration. if (array_key_exists($property, $custom_map)) { $map[$property] = $custom_map[$property]; continue; } // If we don't have an explicit type, try a builtin type for the // column. $type = idx($builtin, $property); if ($type) { $map[$property] = $type; continue; } // If the column has serialization, we can infer the column type. if (isset($serialization[$property])) { $type = idx($serialization_map, $serialization[$property]); if ($type) { $map[$property] = $type; continue; } } if (isset($binary_map[$property])) { $map[$property] = 'bytes'; continue; } if ($property === 'spacePHID') { $map[$property] = 'phid?'; continue; } // If the column is named `somethingPHID`, infer it is a PHID. if (preg_match('/[a-z]PHID$/', $property)) { $map[$property] = 'phid'; continue; } // If the column is named `somethingID`, infer it is an ID. if (preg_match('/[a-z]ID$/', $property)) { $map[$property] = 'id'; continue; } // We don't know the type of this column. $map[$property] = PhabricatorConfigSchemaSpec::DATATYPE_UNKNOWN; } return $map; } public function getSchemaKeys() { $custom_map = $this->getConfigOption(self::CONFIG_KEY_SCHEMA); if (!$custom_map) { $custom_map = array(); } $default_map = array(); foreach ($this->getAllLiskProperties() as $property) { switch ($property) { case 'id': $default_map['PRIMARY'] = array( 'columns' => array('id'), 'unique' => true, ); break; case 'phid': $default_map['key_phid'] = array( 'columns' => array('phid'), 'unique' => true, ); break; case 'spacePHID': $default_map['key_space'] = array( 'columns' => array('spacePHID'), ); break; } } return $custom_map + $default_map; } public function getColumnMaximumByteLength($column) { $map = $this->getSchemaColumns(); if (!isset($map[$column])) { throw new Exception( pht( 'Object (of class "%s") does not have a column "%s".', get_class($this), $column)); } $data_type = $map[$column]; return id(new PhabricatorStorageSchemaSpec()) ->getMaximumByteLengthForDataType($data_type); } } diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php index b7a4c1453b..7972acc0a8 100644 --- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php +++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php @@ -1,420 +1,420 @@ getPasswordHash($password)->openEnvelope(); $expect_hash = $hash->openEnvelope(); return phutil_hashes_are_identical($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 Hasher objects. * @task hashing */ public static function getAllHashers() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getHashName') ->execute(); 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)); } } return $objects; } /** * Get all usable password hashers. This may include hashers which are * not desirable or advisable. * * @return list 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. + * @return PhabricatorPasswordHasher 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. + * @return PhabricatorPasswordHasher 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'); } } } diff --git a/src/view/form/control/AphrontFormTextAreaControl.php b/src/view/form/control/AphrontFormTextAreaControl.php index 88eed462f3..2665c6aaa7 100644 --- a/src/view/form/control/AphrontFormTextAreaControl.php +++ b/src/view/form/control/AphrontFormTextAreaControl.php @@ -1,98 +1,98 @@ sigil = $sigil; return $this; } public function getSigil() { return $this->sigil; } public function setPlaceHolder($place_holder) { $this->placeHolder = $place_holder; return $this; } private function getPlaceHolder() { return $this->placeHolder; } public function setHeight($height) { $this->height = $height; return $this; } public function setReadOnly($read_only) { $this->readOnly = $read_only; return $this; } protected function getReadOnly() { return $this->readOnly; } protected function getCustomControlClass() { return 'aphront-form-control-textarea'; } public function setCustomClass($custom_class) { $this->customClass = $custom_class; return $this; } protected function renderInput() { $height_class = null; switch ($this->height) { case self::HEIGHT_VERY_SHORT: case self::HEIGHT_SHORT: case self::HEIGHT_VERY_TALL: $height_class = 'aphront-textarea-'.$this->height; break; } $classes = array(); $classes[] = $height_class; $classes[] = $this->customClass; $classes = trim(implode(' ', $classes)); // NOTE: This needs to be string cast, because if we pass `null` the // tag will be self-closed and some browsers aren't thrilled about that. $value = (string)$this->getValue(); // NOTE: We also need to prefix the string with a newline, because browsers // ignore a newline immediately after a