diff --git a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php index cedf46bd3..5834984a6 100644 --- a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php @@ -1,93 +1,88 @@ accountKey = idx($data, 'akey'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); list($account, $provider, $response) = $result; if ($response) { return $response; } if (!$provider->shouldAllowAccountLink()) { return $this->renderError(pht('This account is not linkable.')); } $panel_uri = '/settings/panel/external/'; if ($request->isFormPost()) { $account->setUserPHID($viewer->getPHID()); $account->save(); $this->clearRegistrationCookies(); // TODO: Send the user email about the new account link. return id(new AphrontRedirectResponse())->setURI($panel_uri); } // TODO: Provide more information about the external account. Clicking // through this form blindly is dangerous. // TODO: If the user has password authentication, require them to retype // their password here. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Confirm %s Account Link', $provider->getProviderName())) ->addCancelButton($panel_uri) ->addSubmitButton(pht('Confirm Account Link')); $form = id(new PHUIFormLayoutView()) ->setFullWidth(true) ->appendChild( phutil_tag( 'div', array( 'class' => 'aphront-form-instructions', ), pht( "Confirm the link with this %s account. This account will be ". "able to log in to your Phabricator account.", $provider->getProviderName()))) ->appendChild( id(new PhabricatorAuthAccountView()) ->setUser($viewer) ->setExternalAccount($account) ->setAuthProvider($provider)); $dialog->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Confirm Link')) - ->setHref($panel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb(pht('Confirm Link'), $panel_uri); + $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Confirm External Account Link'), 'device' => true, )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php index 326474fe2..3cb06928a 100644 --- a/src/applications/auth/controller/PhabricatorAuthLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php @@ -1,137 +1,132 @@ providerKey = $data['pkey']; $this->action = $data['action']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $this->providerKey); if (!$provider) { return new Aphront404Response(); } switch ($this->action) { case 'link': if (!$provider->shouldAllowAccountLink()) { return $this->renderErrorPage( pht('Account Not Linkable'), array( pht('This provider is not configured to allow linking.'), )); } break; case 'refresh': if (!$provider->shouldAllowAccountRefresh()) { return $this->renderErrorPage( pht('Account Not Refreshable'), array( pht('This provider does not allow refreshing.'), )); } break; default: return new Aphront400Response(); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND userPHID = %s', $provider->getProviderType(), $provider->getProviderDomain(), $viewer->getPHID()); switch ($this->action) { case 'link': if ($account) { return $this->renderErrorPage( pht('Account Already Linked'), array( pht( 'Your Phabricator account is already linked to an external '. 'account for this provider.'), )); } break; case 'refresh': if (!$account) { return $this->renderErrorPage( pht('No Account Linked'), array( pht( 'You do not have a linked account on this provider, and thus '. 'can not refresh it.'), )); } break; default: return new Aphront400Response(); } $panel_uri = '/settings/panel/external/'; $request->setCookie('phcid', Filesystem::readRandomCharacters(16)); switch ($this->action) { case 'link': $form = $provider->buildLinkForm($this); break; case 'refresh': $form = $provider->buildRefreshForm($this); break; default: return new Aphront400Response(); } if ($provider->isLoginFormAButton()) { require_celerity_resource('auth-css'); $form = phutil_tag( 'div', array( 'class' => 'phabricator-link-button pl', ), $form); } switch ($this->action) { case 'link': $name = pht('Link Account'); $title = pht('Link %s Account', $provider->getProviderName()); break; case 'refresh': $name = pht('Refresh Account'); $title = pht('Refresh %s Account', $provider->getProviderName()); break; default: return new Aphront400Response(); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Link Account')) - ->setHref($panel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName($name))); + $crumbs->addTextCrumb(pht('Link Account'), $panel_uri); + $crumbs->addTextCrumb($provider->getProviderName($name)); return $this->buildApplicationPage( array( $crumbs, $form, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index deb71a75b..e95babeea 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -1,243 +1,235 @@ providerKey = $data['pkey']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $response = $this->loadProvider(); if ($response) { return $response; } $provider = $this->provider; try { list($account, $response) = $provider->processLoginRequest($this); } catch (PhutilAuthUserAbortedException $ex) { if ($viewer->isLoggedIn()) { // If a logged-in user cancels, take them back to the external accounts // panel. $next_uri = '/settings/panel/external/'; } else { // If a logged-out user cancels, take them back to the auth start page. $next_uri = '/'; } // User explicitly hit "Cancel". $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authentication Canceled')) ->appendChild( pht('You canceled authentication.')) ->addCancelButton($next_uri, pht('Continue')); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($response) { return $response; } if (!$account) { throw new Exception( "Auth provider failed to load an account from processLoginRequest()!"); } if ($account->getUserPHID()) { // The account is already attached to a Phabricator user, so this is // either a login or a bad account link request. if (!$viewer->isLoggedIn()) { if ($provider->shouldAllowLogin()) { return $this->processLoginUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow logins on this Phabricator install. '. 'An administrator may have recently disabled it.', $provider->getProviderName())); } } else if ($viewer->getPHID() == $account->getUserPHID()) { // This is either an attempt to re-link an existing and already // linked account (which is silly) or a refresh of an external account // (e.g., an OAuth account). return id(new AphrontRedirectResponse()) ->setURI('/settings/panel/external/'); } else { return $this->renderError( pht( 'The external account ("%s") you just used to login is alerady '. 'associated with another Phabricator user account. Login to the '. 'other Phabricator account and unlink the external account before '. 'linking it to a new Phabricator account.', $provider->getProviderName())); } } else { // The account is not yet attached to a Phabricator user, so this is // either a registration or an account link request. if (!$viewer->isLoggedIn()) { if ($provider->shouldAllowRegistration()) { return $this->processRegisterUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow registration on this Phabricator '. 'install. An administrator may have recently disabled it.', $provider->getProviderName())); } } else { if ($provider->shouldAllowAccountLink()) { return $this->processLinkUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow account linking on this Phabricator '. 'install. An administrator may have recently disabled it.', $provider->getProviderName())); } } } // This should be unreachable, but fail explicitly if we get here somehow. return new Aphront400Response(); } private function processLoginUser(PhabricatorExternalAccount $account) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $account->getUserPHID()); if (!$user) { return $this->renderError( pht( 'The external account you just logged in with is not associated '. 'with a valid Phabricator user.')); } return $this->loginUser($user); } private function processRegisterUser(PhabricatorExternalAccount $account) { $account_secret = $account->getAccountSecret(); $register_uri = $this->getApplicationURI('register/'.$account_secret.'/'); return $this->setAccountKeyAndContinue($account, $register_uri); } private function processLinkUser(PhabricatorExternalAccount $account) { $account_secret = $account->getAccountSecret(); $confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/'); return $this->setAccountKeyAndContinue($account, $confirm_uri); } private function setAccountKeyAndContinue( PhabricatorExternalAccount $account, $next_uri) { if ($account->getUserPHID()) { throw new Exception("Account is already registered or linked."); } // Regenerate the registration secret key, set it on the external account, // set a cookie on the user's machine, and redirect them to registration. // See PhabricatorAuthRegisterController for discussion of the registration // key. $registration_key = Filesystem::readRandomCharacters(32); $account->setProperty( 'registrationKey', PhabricatorHash::digest($registration_key)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); $this->getRequest()->setCookie('phreg', $registration_key); return id(new AphrontRedirectResponse())->setURI($next_uri); } private function loadProvider() { $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $this->providerKey); if (!$provider) { return $this->renderError( pht( 'The account you are attempting to login with uses a nonexistent '. 'or disabled authentication provider (with key "%s"). An '. 'administrator may have recently disabled this provider.', $this->providerKey)); } $this->provider = $provider; return null; } protected function renderError($message) { return $this->renderErrorPage( pht('Login Failed'), array($message)); } public function buildProviderPageResponse( PhabricatorAuthProvider $provider, $content) { $crumbs = $this->buildApplicationCrumbs(); if ($this->getRequest()->getUser()->isLoggedIn()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Link Account')) - ->setHref($provider->getSettingsURI())); + $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Login')) - ->setHref($this->getApplicationURI('start/'))); + $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => pht('Login'), 'device' => true, )); } public function buildProviderErrorResponse( PhabricatorAuthProvider $provider, $message) { $message = pht( 'Authentication provider ("%s") encountered an error during login. %s', $provider->getProviderName(), $message); return $this->renderError($message); } } diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 868aa1e30..62e629079 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -1,572 +1,566 @@ accountKey = idx($data, 'akey'); } public function processRequest() { $request = $this->getRequest(); if ($request->getUser()->isLoggedIn()) { return $this->renderError(pht('You are already logged in.')); } $is_setup = false; if (strlen($this->accountKey)) { $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); list($account, $provider, $response) = $result; $is_default = false; } else if ($this->isFirstTimeSetup()) { list($account, $provider, $response) = $this->loadSetupAccount(); $is_default = true; $is_setup = true; } else { list($account, $provider, $response) = $this->loadDefaultAccount(); $is_default = true; } if ($response) { return $response; } if (!$provider->shouldAllowRegistration()) { // TODO: This is a routine error if you click "Login" on an external // auth source which doesn't allow registration. The error should be // more tailored. return $this->renderError( pht( 'The account you are attempting to register with uses an '. 'authentication provider ("%s") which does not allow registration. '. 'An administrator may have recently disabled registration with this '. 'provider.', $provider->getProviderName())); } $user = new PhabricatorUser(); $default_username = $account->getUsername(); $default_realname = $account->getRealName(); $default_email = $account->getEmail(); if ($default_email) { // If the account source provided an email, but it's not allowed by // the configuration, roadblock the user. Previously, we let the user // pick a valid email address instead, but this does not align well with // user expectation and it's not clear the cases it enables are valuable. // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { return $this->renderError( array( pht( 'The account you are attempting to register with has an invalid '. 'email address (%s). This Phabricator install only allows '. 'registration with specific email addresses:', $default_email), phutil_tag('br'), phutil_tag('br'), PhabricatorUserEmail::describeAllowedAddresses(), )); } // If the account source provided an email, but another account already // has that email, just pretend we didn't get an email. // TODO: See T3340. // TODO: See T3472. if ($default_email) { $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $default_email); if ($same_email) { $default_email = null; } } } $profile = id(new PhabricatorRegistrationProfile()) ->setDefaultUsername($default_username) ->setDefaultEmail($default_email) ->setDefaultRealName($default_realname) ->setCanEditUsername(true) ->setCanEditEmail(($default_email === null)) ->setCanEditRealName(true) ->setShouldVerifyEmail(false); $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER; $event_data = array( 'account' => $account, 'profile' => $profile, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $default_username = $profile->getDefaultUsername(); $default_email = $profile->getDefaultEmail(); $default_realname = $profile->getDefaultRealName(); $can_edit_username = $profile->getCanEditUsername(); $can_edit_email = $profile->getCanEditEmail(); $can_edit_realname = $profile->getCanEditRealName(); $must_set_password = $provider->shouldRequireRegistrationPassword(); $can_edit_anything = $profile->getCanEditAnything() || $must_set_password; $force_verify = $profile->getShouldVerifyEmail(); $value_username = $default_username; $value_realname = $default_realname; $value_email = $default_email; $value_password = null; $errors = array(); $e_username = strlen($value_username) ? null : true; $e_realname = strlen($value_realname) ? null : true; $e_email = strlen($value_email) ? null : true; $e_password = true; $e_captcha = true; $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; if ($request->isFormPost() || !$can_edit_anything) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); if ($must_set_password) { $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht("Captcha response is incorrect, try again."); $e_captcha = pht('Invalid'); } } if ($can_edit_username) { $value_username = $request->getStr('username'); if (!strlen($value_username)) { $e_username = pht('Required'); $errors[] = pht('Username is required.'); } else if (!PhabricatorUser::validateUsername($value_username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } else { $e_username = null; } } if ($must_set_password) { $value_password = $request->getStr('password'); $value_confirm = $request->getStr('confirm'); if (!strlen($value_password)) { $e_password = pht('Required'); $errors[] = pht('You must choose a password.'); } else if ($value_password !== $value_confirm) { $e_password = pht('No Match'); $errors[] = pht('Password and confirmation must match.'); } else if (strlen($value_password) < $min_len) { $e_password = pht('Too Short'); $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); } else { $e_password = null; } } if ($can_edit_email) { $value_email = $request->getStr('email'); if (!strlen($value_email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } if ($can_edit_realname) { $value_realname = $request->getStr('realName'); if (!strlen($value_realname)) { $e_realname = pht('Required'); $errors[] = pht('Real name is required.'); } else { $e_realname = null; } } if (!$errors) { $image = $this->loadProfilePicture($account); if ($image) { $user->setProfileImagePHID($image->getPHID()); } try { if ($force_verify) { $verify_email = true; } else { $verify_email = ($account->getEmailVerified()) && ($value_email === $default_email); } $email_obj = id(new PhabricatorUserEmail()) ->setAddress($value_email) ->setIsVerified((int)$verify_email); $user->setUsername($value_username); $user->setRealname($value_realname); if ($is_setup) { $must_approve = false; } else { $must_approve = PhabricatorEnv::getEnvConfig( 'auth.require-approval'); } if ($must_approve) { $user->setIsApproved(0); } else { $user->setIsApproved(1); } $user->openTransaction(); $editor = id(new PhabricatorUserEditor()) ->setActor($user); $editor->createNewUser($user, $email_obj); if ($must_set_password) { $envelope = new PhutilOpaqueEnvelope($value_password); $editor->changePassword($user, $envelope); } if ($is_setup) { $editor->makeAdminUser($user, true); } $account->setUserPHID($user->getPHID()); $provider->willRegisterAccount($account); $account->save(); $user->saveTransaction(); if (!$email_obj->getIsVerified()) { $email_obj->sendVerificationEmail($user); } if ($must_approve) { $this->sendWaitingForApprovalEmail($user); } return $this->loginUser($user); } catch (AphrontQueryDuplicateKeyException $exception) { $same_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user->getUserName()); $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $value_email); if ($same_username) { $e_username = pht('Duplicate'); $errors[] = pht('Another user already has that username.'); } if ($same_email) { // TODO: See T3340. $e_email = pht('Duplicate'); $errors[] = pht('Another user already has that email.'); } if (!$same_username && !$same_email) { throw $exception; } } } unset($unguarded); } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Registration Failed')); $error_view->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($request->getUser()); if (!$is_default) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('External Account')) ->setValue( id(new PhabricatorAuthAccountView()) ->setUser($request->getUser()) ->setExternalAccount($account) ->setAuthProvider($provider))); } if ($can_edit_username) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Phabricator Username')) ->setValue($value_username) ->setError($e_username)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password') ->setError($e_password) ->setCaption( $min_len ? pht('Minimum length of %d characters.', $min_len) : null)); $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Confirm Password')) ->setName('confirm') ->setError($e_password)); } if ($can_edit_email) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($value_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } if ($can_edit_realname) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realName') ->setValue($value_realname) ->setError($e_realname)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); } $submit = id(new AphrontFormSubmitControl()); if ($is_setup) { $submit ->setValue(pht('Create Admin Account')); } else { $submit ->addCancelButton($this->getApplicationURI('start/')) ->setValue(pht('Register Phabricator Account')); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($is_setup) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup Admin Account'))); + $crumbs->addTextCrumb(pht('Setup Admin Account')); $title = pht('Welcome to Phabricator'); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Register'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($provider->getProviderName())); + $crumbs->addTextCrumb(pht('Register')); + $crumbs->addTextCrumb($provider->getProviderName()); $title = pht('Phabricator Registration'); } $welcome_view = null; if ($is_setup) { $welcome_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Welcome to Phabricator')) ->appendChild( pht( 'Installation is complete. Register your administrator account '. 'below to log in. You will be able to configure options and add '. 'other authentication mechanisms (like LDAP or OAuth) later on.')); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormError($error_view); return $this->buildApplicationPage( array( $crumbs, $welcome_view, $object_box, ), array( 'title' => $title, 'device' => true, )); } private function loadDefaultAccount() { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { if (!$candidate_provider->shouldAllowRegistration()) { unset($providers[$key]); continue; } if (!$candidate_provider->isDefaultRegistrationProvider()) { unset($providers[$key]); } } if (!$providers) { $response = $this->renderError( pht( "There are no configured default registration providers.")); return array($account, $provider, $response); } else if (count($providers) > 1) { $response = $this->renderError( pht( "There are too many configured default registration providers.")); return array($account, $provider, $response); } $provider = head($providers); $account = $provider->getDefaultExternalAccount(); return array($account, $provider, $response); } private function loadSetupAccount() { $provider = new PhabricatorAuthProviderPassword(); $provider->attachProviderConfig( id(new PhabricatorAuthProviderConfig()) ->setShouldAllowRegistration(1) ->setShouldAllowLogin(1) ->setIsEnabled(true)); $account = $provider->getDefaultExternalAccount(); $response = null; return array($account, $provider, $response); } private function loadProfilePicture(PhabricatorExternalAccount $account) { $phid = $account->getProfileImagePHID(); if (!$phid) { return null; } // NOTE: Use of omnipotent user is okay here because the registering user // can not control the field value, and we can't use their user object to // do meaningful policy checks anyway since they have not registered yet. // Reaching this means the user holds the account secret key and the // registration secret key, and thus has permission to view the image. $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return null; } try { $xformer = new PhabricatorImageTransformer(); return $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } catch (Exception $ex) { phlog($ex); return null; } } protected function renderError($message) { return $this->renderErrorPage( pht('Registration Failed'), array($message)); } private function sendWaitingForApprovalEmail(PhabricatorUser $user) { $title = '[Phabricator] '.pht( 'New User "%s" Awaiting Approval', $user->getUsername()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection( pht( 'Newly registered user "%s" is awaiting account approval by an '. 'administrator.', $user->getUsername())); $body->addTextSection( pht('APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/people/query/approval/')); $body->addTextSection( pht('DISABLE APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/config/edit/auth.require-approval/')); $admins = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIsAdmin(true) ->execute(); if (!$admins) { return; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(mpull($admins, 'getPHID')) ->setSubject($title) ->setBody($body->render()) ->saveAndSend(); } } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 468e1f861..81f10d748 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -1,198 +1,196 @@ getRequest(); $viewer = $request->getUser(); if ($viewer->isLoggedIn()) { // Kick the user home if they are already logged in. return id(new AphrontRedirectResponse())->setURI('/'); } if ($request->isAjax()) { return $this->processAjaxRequest(); } if ($request->isConduit()) { return $this->processConduitRequest(); } if ($request->getCookie('phusr') && $request->getCookie('phsid')) { // The session cookie is invalid, so clear it. $request->clearCookie('phusr'); $request->clearCookie('phsid'); return $this->renderError( pht( "Your login session is invalid. Try reloading the page and logging ". "in again. If that does not work, clear your browser cookies.")); } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($providers as $key => $provider) { if (!$provider->shouldAllowLogin()) { unset($providers[$key]); } } if (!$providers) { if ($this->isFirstTimeSetup()) { // If this is a fresh install, let the user register their admin // account. return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('/register/')); } return $this->renderError( pht( "This Phabricator install is not configured with any enabled ". "authentication providers which can be used to log in. If you ". "have accidentally locked yourself out by disabling all providers, ". "you can use `phabricator/bin/auth recover ` to ". "recover access to an administrative account.")); } $next_uri = $request->getStr('next'); if (!$next_uri) { $next_uri_path = $this->getRequest()->getPath(); if ($next_uri_path == '/auth/start/') { $next_uri = '/'; } else { $next_uri = $this->getRequest()->getRequestURI(); } } if (!$request->isFormPost()) { $request->setCookie('next_uri', $next_uri); $request->setCookie('phcid', Filesystem::readRandomCharacters(16)); } $not_buttons = array(); $are_buttons = array(); $providers = msort($providers, 'getLoginOrder'); foreach ($providers as $provider) { if ($provider->isLoginFormAButton()) { $are_buttons[] = $provider->buildLoginForm($this); } else { $not_buttons[] = $provider->buildLoginForm($this); } } $out = array(); $out[] = $not_buttons; if ($are_buttons) { require_celerity_resource('auth-css'); foreach ($are_buttons as $key => $button) { $are_buttons[$key] = phutil_tag( 'div', array( 'class' => 'phabricator-login-button mmb', ), $button); } // If we only have one button, add a second pretend button so that we // always have two columns. This makes it easier to get the alignments // looking reasonable. if (count($are_buttons) == 1) { $are_buttons[] = null; } $button_columns = id(new AphrontMultiColumnView()) ->setFluidLayout(true); $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2)); foreach ($are_buttons as $column) { $button_columns->addColumn($column); } $out[] = phutil_tag( 'div', array( 'class' => 'phabricator-login-buttons', ), $button_columns); } $login_message = PhabricatorEnv::getEnvConfig('auth.login-message'); $login_message = phutil_safe_html($login_message); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Login'))); + $crumbs->addTextCrumb(pht('Login')); return $this->buildApplicationPage( array( $crumbs, $login_message, $out, ), array( 'title' => pht('Login to Phabricator'), 'device' => true, )); } private function processAjaxRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // We end up here if the user clicks a workflow link that they need to // login to use. We give them a dialog saying "You need to login...". if ($request->isDialogFormPost()) { return id(new AphrontRedirectResponse())->setURI( $request->getRequestURI()); } $dialog = new AphrontDialogView(); $dialog->setUser($viewer); $dialog->setTitle(pht('Login Required')); $dialog->appendChild(pht('You must login to continue.')); $dialog->addSubmitButton(pht('Login')); $dialog->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } private function processConduitRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // A common source of errors in Conduit client configuration is getting // the request path wrong. The client will end up here, so make some // effort to give them a comprehensible error message. $request_path = $this->getRequest()->getPath(); $conduit_path = '/api/'; $example_path = '/api/conduit.ping'; $message = pht( 'ERROR: You are making a Conduit API request to "%s", but the correct '. 'HTTP request path to use in order to access a COnduit method is "%s" '. '(for example, "%s"). Check your configuration.', $request_path, $conduit_path, $example_path); return id(new AphrontPlainTextResponse())->setContent($message); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Failure'), array($message)); } } diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 71ba31657..8dd7be7c2 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -1,159 +1,157 @@ getRequest(); if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { return new Aphront400Response(); } $e_email = true; $e_captcha = true; $errors = array(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($request->isFormPost()) { $e_email = null; $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht("Captcha response is incorrect, try again."); $e_captcha = pht('Invalid'); } $email = $request->getStr('email'); if (!strlen($email)) { $errors[] = pht("You must provide an email address."); $e_email = pht('Required'); } if (!$errors) { // NOTE: Don't validate the email unless the captcha is good; this makes // it expensive to fish for valid email addresses while giving the user // a better error if they goof their email. $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); $target_user = null; if ($target_email) { $target_user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $target_email->getUserPHID()); } if (!$target_user) { $errors[] = pht("There is no account associated with that email address."); $e_email = pht("Invalid"); } if (!$errors) { $uri = $target_user->getEmailLoginURI($target_email); if ($is_serious) { $body = <<setSubject('[Phabricator] Password Reset'); $mail->addTos( array( $target_user->getPHID(), )); $mail->setBody($body); $mail->saveAndSend(); $view = new AphrontRequestFailureView(); $view->setHeader(pht('Check Your Email')); $view->appendChild(phutil_tag('p', array(), pht( 'An email has been sent with a link you can use to login.'))); return $this->buildStandardPageResponse( $view, array( 'title' => pht('Email Sent'), )); } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); } $email_auth = new PHUIFormLayoutView(); $email_auth->appendChild($error_view); $email_auth ->setUser($request->getUser()) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($request->getStr('email')) ->setError($e_email)) ->appendChild( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Reset Password'))); + $crumbs->addTextCrumb(pht('Reset Password')); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle(pht( 'Forgot Password / Email Login')); $dialog->appendChild($email_auth); $dialog->addSubmitButton(pht('Send Email')); $dialog->setSubmitURI('/login/email/'); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Forgot Password'), 'device' => true, )); } } diff --git a/src/applications/auth/controller/PhabricatorEmailVerificationController.php b/src/applications/auth/controller/PhabricatorEmailVerificationController.php index 4bbe9b2dc..0dbb91885 100644 --- a/src/applications/auth/controller/PhabricatorEmailVerificationController.php +++ b/src/applications/auth/controller/PhabricatorEmailVerificationController.php @@ -1,100 +1,98 @@ code = $data['code']; } public function shouldRequireEmailVerification() { // Since users need to be able to hit this endpoint in order to verify // email, we can't ever require email verification here. return false; } public function shouldRequireEnabledUser() { // Unapproved users are allowed to verify their email addresses. We'll kick // disabled users out later. return false; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($user->getIsDisabled()) { // We allowed unapproved and disabled users to hit this controller, but // want to kick out disabled users now. return new Aphront400Response(); } $email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND verificationCode = %s', $user->getPHID(), $this->code); if (!$email) { $title = pht('Unable to Verify Email'); $content = pht( 'The verification code you provided is incorrect, or the email '. 'address has been removed, or the email address is owned by another '. 'user. Make sure you followed the link in the email correctly and are '. 'logged in with the user account associated with the email address.'); $continue = pht('Rats!'); } else if ($email->getIsVerified() && $user->getIsEmailVerified()) { $title = pht('Address Already Verified'); $content = pht( 'This email address has already been verified.'); $continue = pht('Continue to Phabricator'); } else { $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $email->openTransaction(); $email->setIsVerified(1); $email->save(); // If the user just verified their primary email address, mark their // account as email verified. $user_primary = $user->loadPrimaryEmail(); if ($user_primary->getID() == $email->getID()) { $user->setIsEmailVerified(1); $user->save(); } $email->saveTransaction(); unset($guard); $title = pht('Address Verified'); $content = pht( 'The email address %s is now verified.', phutil_tag('strong', array(), $email->getAddress())); $continue = pht('Continue to Phabricator'); } $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle($title) ->setMethod('GET') ->addCancelButton('/', $continue) ->appendChild($content); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Verify Email'))); + $crumbs->addTextCrumb(pht('Verify Email')); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Verify Email'), 'device' => true, )); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index 0ea5b0e54..b7a79e220 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -1,322 +1,320 @@ providerClass = idx($data, 'className'); $this->configID = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->configID) { $config = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->configID)) ->executeOne(); if (!$config) { return new Aphront404Response(); } $provider = $config->getProvider(); if (!$provider) { return new Aphront404Response(); } $is_new = false; } else { $providers = PhabricatorAuthProvider::getAllBaseProviders(); foreach ($providers as $candidate_provider) { if (get_class($candidate_provider) === $this->providerClass) { $provider = $candidate_provider; break; } } if (!$provider) { return new Aphront404Response(); } // TODO: When we have multi-auth providers, support them here. $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->withProviderClasses(array(get_class($provider))) ->execute(); if ($configs) { $id = head($configs)->getID(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setMethod('GET') ->setSubmitURI($this->getApplicationURI('config/edit/'.$id.'/')) ->setTitle(pht('Provider Already Configured')) ->appendChild( pht( 'This provider ("%s") already exists, and you can not add more '. 'than one instance of it. You can edit the existing provider, '. 'or you can choose a different provider.', $provider->getProviderName())) ->addCancelButton($this->getApplicationURI('config/new/')) ->addSubmitButton(pht('Edit Existing Provider')); return id(new AphrontDialogResponse())->setDialog($dialog); } $config = $provider->getDefaultProviderConfig(); $provider->attachProviderConfig($config); $is_new = true; } $errors = array(); $v_registration = $config->getShouldAllowRegistration(); $v_link = $config->getShouldAllowLink(); $v_unlink = $config->getShouldAllowUnlink(); if ($request->isFormPost()) { $properties = $provider->readFormValuesFromRequest($request); list($errors, $issues, $properties) = $provider->processEditForm( $request, $properties); $xactions = array(); if (!$errors) { if ($is_new) { if (!strlen($config->getProviderType())) { $config->setProviderType($provider->getProviderType()); } if (!strlen($config->getProviderDomain())) { $config->setProviderDomain($provider->getProviderDomain()); } } $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_REGISTRATION) ->setNewValue($request->getInt('allowRegistration', 0)); $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_LINK) ->setNewValue($request->getInt('allowLink', 0)); $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_UNLINK) ->setNewValue($request->getInt('allowUnlink', 0)); foreach ($properties as $key => $value) { $xactions[] = id(new PhabricatorAuthProviderConfigTransaction()) ->setTransactionType( PhabricatorAuthProviderConfigTransaction::TYPE_PROPERTY) ->setMetadataValue('auth:property', $key) ->setNewValue($value); } if ($is_new) { $config->save(); } $editor = id(new PhabricatorAuthProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($config, $xactions); if ($provider->hasSetupStep() && $is_new) { $id = $config->getID(); $next_uri = $this->getApplicationURI('config/edit/'.$id.'/'); } else { $next_uri = $this->getApplicationURI(); } return id(new AphrontRedirectResponse())->setURI($next_uri); } } else { $properties = $provider->readFormValuesFromProvider(); $issues = array(); } if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } if ($is_new) { $button = pht('Add Provider'); $crumb = pht('Add Provider'); $title = pht('Add Authentication Provider'); $cancel_uri = $this->getApplicationURI('/config/new/'); } else { $button = pht('Save'); $crumb = pht('Edit Provider'); $title = pht('Edit Authentication Provider'); $cancel_uri = $this->getApplicationURI(); } $config_name = 'auth.email-domains'; $config_href = '/config/edit/'.$config_name.'/'; $email_domains = PhabricatorEnv::getEnvConfig($config_name); if ($email_domains) { $registration_warning = pht( "Users will only be able to register with a verified email address ". "at one of the configured [[ %s | %s ]] domains: **%s**", $config_href, $config_name, implode(', ', $email_domains)); } else { $registration_warning = pht( "NOTE: Any user who can browse to this install's login page will be ". "able to register a Phabricator account. To restrict who can register ". "an account, configure [[ %s | %s ]].", $config_href, $config_name); } $str_registration = array( phutil_tag('strong', array(), pht('Allow Registration:')), ' ', pht( 'Allow users to register new Phabricator accounts using this '. 'provider. If you disable registration, users can still use this '. 'provider to log in to existing accounts, but will not be able to '. 'create new accounts.'), ); $str_link = hsprintf( '%s: %s', pht('Allow Linking Accounts'), pht( 'Allow users to link account credentials for this provider to '. 'existing Phabricator accounts. There is normally no reason to '. 'disable this unless you are trying to move away from a provider '. 'and want to stop users from creating new account links.')); $str_unlink = hsprintf( '%s: %s', pht('Allow Unlinking Accounts'), pht( 'Allow users to unlink account credentials for this provider from '. 'existing Phabricator accounts. If you disable this, Phabricator '. 'accounts will be permanently bound to provider accounts.')); $status_tag = id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE); if ($is_new) { $status_tag ->setName(pht('New Provider')) ->setBackgroundColor('blue'); } else if ($config->getIsEnabled()) { $status_tag ->setName(pht('Enabled')) ->setBackgroundColor('green'); } else { $status_tag ->setName(pht('Disabled')) ->setBackgroundColor('red'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Provider')) ->setValue($provider->getProviderName())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Status')) ->setValue($status_tag)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Allow')) ->addCheckbox( 'allowRegistration', 1, $str_registration, $v_registration)) ->appendRemarkupInstructions($registration_warning) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'allowLink', 1, $str_link, $v_link)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'allowUnlink', 1, $str_unlink, $v_unlink)); $provider->extendEditForm($request, $form, $properties, $issues); $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button)); $help = $provider->getConfigurationHelp(); if ($help) { $form->appendChild(id(new PHUIFormDividerControl())); $form->appendRemarkupInstructions($help); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($crumb)); + $crumbs->addTextCrumb($crumb); $xaction_view = null; if (!$is_new) { $xactions = id(new PhabricatorAuthProviderConfigTransactionQuery()) ->withObjectPHIDs(array($config->getPHID())) ->setViewer($viewer) ->execute(); foreach ($xactions as $xaction) { $xaction->setProvider($provider); } $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($config->getPHID()) ->setTransactions($xactions); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $xaction_view, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index 7c24bbb4a..421e3197a 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -1,138 +1,136 @@ getRequest(); $viewer = $request->getUser(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->execute(); $list = new PHUIObjectItemListView(); foreach ($configs as $config) { $item = new PHUIObjectItemView(); $id = $config->getID(); $edit_uri = $this->getApplicationURI('config/edit/'.$id.'/'); $enable_uri = $this->getApplicationURI('config/enable/'.$id.'/'); $disable_uri = $this->getApplicationURI('config/disable/'.$id.'/'); $provider = $config->getProvider(); if ($provider) { $name = $provider->getProviderName(); } else { $name = $config->getProviderType().' ('.$config->getProviderClass().')'; } $item ->setHeader($name); if ($provider) { $item->setHref($edit_uri); } else { $item->addAttribute(pht('Provider Implementation Missing!')); } $domain = null; if ($provider) { $domain = $provider->getProviderDomain(); if ($domain !== 'self') { $item->addAttribute($domain); } } if ($config->getShouldAllowRegistration()) { $item->addAttribute(pht('Allows Registration')); } if ($config->getIsEnabled()) { $item->setBarColor('green'); $item->addAction( id(new PHUIListItemView()) ->setIcon('delete') ->setHref($disable_uri) ->addSigil('workflow')); } else { $item->setBarColor('grey'); $item->addIcon('delete-grey', pht('Disabled')); $item->addAction( id(new PHUIListItemView()) ->setIcon('new') ->setHref($enable_uri) ->addSigil('workflow')); } $list->addItem($item); } $list->setNoDataString( pht( '%s You have not added authentication providers yet. Use "%s" to add '. 'a provider, which will let users register new Phabricator accounts '. 'and log in.', phutil_tag( 'strong', array(), pht('No Providers Configured:')), phutil_tag( 'a', array( 'href' => $this->getApplicationURI('config/new/'), ), pht('Add Authentication Provider')))); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Auth Providers'))); + $crumbs->addTextCrumb(pht('Auth Providers')); $config_name = 'auth.email-domains'; $config_href = '/config/edit/'.$config_name.'/'; $config_link = phutil_tag( 'a', array( 'href' => $config_href, 'target' => '_blank', ), $config_name); $warning = new AphrontErrorView(); $email_domains = PhabricatorEnv::getEnvConfig($config_name); if ($email_domains) { $warning->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $warning->setTitle(pht('Registration is Restricted')); $warning->appendChild( pht( 'Only users with a verified email address at one of the %s domains '. 'will be able to register a Phabricator account: %s', $config_link, phutil_tag('strong', array(), implode(', ', $email_domains)))); } else { $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->setTitle(pht('Anyone Can Register an Account')); $warning->appendChild( pht( 'Anyone who can browse to this Phabricator install will be able to '. 'register an account. To restrict who can register an account, '. 'configure %s.', $config_link)); } return $this->buildApplicationPage( array( $crumbs, $warning, $list, ), array( 'title' => pht('Authentication Providers'), 'device' => true, )); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index 7583c8869..3362f0135 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -1,105 +1,103 @@ getRequest(); $viewer = $request->getUser(); $providers = PhabricatorAuthProvider::getAllBaseProviders(); $e_provider = null; $errors = array(); if ($request->isFormPost()) { $provider_string = $request->getStr('provider'); if (!strlen($provider_string)) { $e_provider = pht('Required'); $errors[] = pht('You must select an authentication provider.'); } else { $found = false; foreach ($providers as $provider) { if (get_class($provider) === $provider_string) { $found = true; break; } } if (!$found) { $e_provider = pht('Invalid'); $errors[] = pht('You must select a valid provider.'); } } if (!$errors) { return id(new AphrontRedirectResponse())->setURI( $this->getApplicationURI('/config/new/'.$provider_string.'/')); } } if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $options = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Provider')) ->setName('provider') ->setError($e_provider); $configured = PhabricatorAuthProvider::getAllProviders(); $configured_classes = array(); foreach ($configured as $configured_provider) { $configured_classes[get_class($configured_provider)] = true; } // Sort providers by login order, and move disabled providers to the // bottom. $providers = msort($providers, 'getLoginOrder'); $providers = array_diff_key($providers, $configured_classes) + $providers; foreach ($providers as $provider) { if (isset($configured_classes[get_class($provider)])) { $disabled = true; $description = pht('This provider is already configured.'); } else { $disabled = false; $description = $provider->getDescriptionForCreate(); } $options->addButton( get_class($provider), $provider->getNameForCreate(), $description, $disabled ? 'disabled' : null, $disabled); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($options) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Continue'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Add Authentication Provider')) ->setFormError($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Add Provider'))); + $crumbs->addTextCrumb(pht('Add Provider')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Add Authentication Provider'), 'device' => true, )); } } diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php index 88ebc89e9..945909bbb 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php @@ -1,41 +1,38 @@ getRequest(); $user = $request->getUser(); $channels = id(new PhabricatorChatLogChannelQuery()) ->setViewer($user) ->execute(); $list = new PHUIObjectItemListView(); foreach ($channels as $channel) { $item = id(new PHUIObjectItemView()) ->setHeader($channel->getChannelName()) ->setHref('/chatlog/channel/'.$channel->getID().'/') ->addAttribute($channel->getServiceName()) ->addAttribute($channel->getServiceType()); $list->addItem($item); } $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Channel List')) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Channel List'), $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $list, ), array( 'title' => pht('Channel List'), 'device' => true, )); } } diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php index 07d414264..b457b6f00 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php @@ -1,329 +1,326 @@ channelID = $data['channelID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $uri = clone $request->getRequestURI(); $uri->setQueryParams(array()); $pager = new AphrontCursorPagerView(); $pager->setURI($uri); $pager->setPageSize(250); $query = id(new PhabricatorChatLogQuery()) ->setViewer($user) ->withChannelIDs(array($this->channelID)); $channel = id(new PhabricatorChatLogChannelQuery()) ->setViewer($user) ->withIDs(array($this->channelID)) ->executeOne(); if (!$channel) { return new Aphront404Response(); } list($after, $before, $map) = $this->getPagingParameters($request, $query); $pager->setAfterID($after); $pager->setBeforeID($before); $logs = $query->executeWithCursorPager($pager); // Show chat logs oldest-first. $logs = array_reverse($logs); // Divide all the logs into blocks, where a block is the same author saying // several things in a row. A block ends when another user speaks, or when // two minutes pass without the author speaking. $blocks = array(); $block = null; $last_author = null; $last_epoch = null; foreach ($logs as $log) { $this_author = $log->getAuthor(); $this_epoch = $log->getEpoch(); // Decide whether we should start a new block or not. $new_block = ($this_author !== $last_author) || ($this_epoch - (60 * 2) > $last_epoch); if ($new_block) { if ($block) { $blocks[] = $block; } $block = array( 'id' => $log->getID(), 'epoch' => $this_epoch, 'author' => $this_author, 'logs' => array($log), ); } else { $block['logs'][] = $log; } $last_author = $this_author; $last_epoch = $this_epoch; } if ($block) { $blocks[] = $block; } // Figure out CSS classes for the blocks. We alternate colors between // lines, and highlight the entire block which contains the target ID or // date, if applicable. foreach ($blocks as $key => $block) { $classes = array(); if ($key % 2) { $classes[] = 'alternate'; } $ids = mpull($block['logs'], 'getID', 'getID'); if (array_intersect_key($ids, $map)) { $classes[] = 'highlight'; } $blocks[$key]['class'] = $classes ? implode(' ', $classes) : null; } require_celerity_resource('phabricator-chatlog-css'); $out = array(); foreach ($blocks as $block) { $author = $block['author']; $author = phutil_utf8_shorten($author, 18); $author = phutil_tag('td', array('class' => 'author'), $author); $href = $uri->alter('at', $block['id']); $timestamp = $block['epoch']; $timestamp = phabricator_datetime($timestamp, $user); $timestamp = phutil_tag( 'a', array( 'href' => $href, 'class' => 'timestamp' ), $timestamp); $message = mpull($block['logs'], 'getMessage'); $message = implode("\n", $message); $message = phutil_tag( 'td', array( 'class' => 'message' ), array( $timestamp, $message)); $out[] = phutil_tag( 'tr', array( 'class' => $block['class'], ), array( $author, $message)); } $links = array(); $first_uri = $pager->getFirstPageURI(); if ($first_uri) { $links[] = phutil_tag( 'a', array( 'href' => $first_uri, ), "\xC2\xAB ". pht("Newest")); } $prev_uri = $pager->getPrevPageURI(); if ($prev_uri) { $links[] = phutil_tag( 'a', array( 'href' => $prev_uri, ), "\xE2\x80\xB9 " . pht("Newer")); } $next_uri = $pager->getNextPageURI(); if ($next_uri) { $links[] = phutil_tag( 'a', array( 'href' => $next_uri, ), pht("Older") . " \xE2\x80\xBA"); } $pager_top = phutil_tag( 'div', array('class' => 'phabricator-chat-log-pager-top'), $links); $pager_bottom = phutil_tag( 'div', array('class' => 'phabricator-chat-log-pager-bottom'), $links); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($channel->getChannelName()) - ->setHref($uri)); + ->addTextCrumb($channel->getChannelName(), $uri); $form = id(new AphrontFormView()) ->setUser($user) ->setMethod('GET') ->setAction($uri) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Date')) ->setName('date') ->setValue($request->getStr('date'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Jump'))); $filter = new AphrontListFilterView(); $filter->appendChild($form); $table = phutil_tag( 'table', array( 'class' => 'phabricator-chat-log' ), $out); $log = phutil_tag( 'div', array( 'class' => 'phabricator-chat-log-panel' ), $table); $jump_link = phutil_tag( 'a', array( 'href' => '#latest' ), pht("Jump to Bottom") . " \xE2\x96\xBE"); $jump = phutil_tag( 'div', array( 'class' => 'phabricator-chat-log-jump' ), $jump_link); $jump_target = phutil_tag( 'div', array( 'id' => 'latest' )); $content = phutil_tag( 'div', array( 'class' => 'phabricator-chat-log-wrap' ), array( $jump, $pager_top, $log, $jump_target, $pager_bottom, )); return $this->buildApplicationPage( array( $crumbs, $filter, $content, ), array( 'title' => pht('Channel Log'), 'device' => true, )); } /** * From request parameters, figure out where we should jump to in the log. * We jump to either a date or log ID, but load a few lines of context before * it so the user can see the nearby conversation. */ private function getPagingParameters( AphrontRequest $request, PhabricatorChatLogQuery $query) { $user = $request->getUser(); $at_id = $request->getInt('at'); $at_date = $request->getStr('date'); $context_log = null; $map = array(); $query = clone $query; $query->setLimit(8); if ($at_id) { // Jump to the log in question, and load a few lines of context before // it. $context_logs = $query ->setAfterID($at_id) ->execute(); $context_log = last($context_logs); $map = array( $at_id => true, ); } else if ($at_date) { $timestamp = PhabricatorTime::parseLocalTime($at_date, $user); if ($timestamp) { $context_logs = $query ->withMaximumEpoch($timestamp) ->execute(); $context_log = last($context_logs); $target_log = head($context_logs); if ($target_log) { $map = array( $target_log->getID() => true, ); } } } if ($context_log) { $after = null; $before = $context_log->getID() - 1; } else { $after = $request->getInt('after'); $before = $request->getInt('before'); } return array($after, $before, $map); } } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index b08873f51..8cfe6815d 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -1,492 +1,486 @@ method = $data['method']; return $this; } public function processRequest() { $time_start = microtime(true); $request = $this->getRequest(); $method = $this->method; $api_request = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); $metadata = array(); try { $params = $this->decodeConduitParams($request, $method); $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); $call = new ConduitCall( $method, $params, idx($metadata, 'isProxied', false)); $result = null; // TODO: Straighten out the auth pathway here. We shouldn't be creating // a ConduitAPIRequest at this level, but some of the auth code expects // it. Landing a halfway version of this to unblock T945. $api_request = new ConduitAPIRequest($params); $allow_unguarded_writes = false; $auth_error = null; $conduit_username = '-'; if ($call->shouldRequireAuthentication()) { $metadata['scope'] = $call->getRequiredScope(); $auth_error = $this->authenticateUser($api_request, $metadata); // If we've explicitly authenticated the user here and either done // CSRF validation or are using a non-web authentication mechanism. $allow_unguarded_writes = true; if (isset($metadata['actAsUser'])) { $this->actAsUser($api_request, $metadata['actAsUser']); } if ($auth_error === null) { $conduit_user = $api_request->getUser(); if ($conduit_user && $conduit_user->getPHID()) { $conduit_username = $conduit_user->getUsername(); } $call->setUser($api_request->getUser()); } } $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { $access_log->setData( array( 'u' => $conduit_username, 'm' => $method, )); } if ($call->shouldAllowUnguardedWrites()) { $allow_unguarded_writes = true; } if ($auth_error === null) { if ($allow_unguarded_writes) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } try { $result = $call->execute(); $error_code = null; $error_info = null; } catch (ConduitException $ex) { $result = null; $error_code = $ex->getMessage(); if ($ex->getErrorDescription()) { $error_info = $ex->getErrorDescription(); } else { $error_info = $call->getErrorDescription($error_code); } } if ($allow_unguarded_writes) { unset($unguarded); } } else { list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { phlog($ex); $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' : 'ERR-CONDUIT-CORE'); $error_info = $ex->getMessage(); } $time_end = microtime(true); $connection_id = null; if (idx($metadata, 'connectionID')) { $connection_id = $metadata['connectionID']; } else if (($method == 'conduit.connect') && $result) { $connection_id = idx($result, 'connectionID'); } $log ->setCallerPHID( isset($conduit_user) ? $conduit_user->getPHID() : null) ->setConnectionID($connection_id) ->setError((string)$error_code) ->setDuration(1000000 * ($time_end - $time_start)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $log->save(); unset($unguarded); $response = id(new ConduitAPIResponse()) ->setResult($result) ->setErrorCode($error_code) ->setErrorInfo($error_info); switch ($request->getStr('output')) { case 'human': return $this->buildHumanReadableResponse( $method, $api_request, $response->toDictionary()); case 'json': default: return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } } /** * Change the api request user to the user that we want to act as. * Only admins can use actAsUser * * @param ConduitAPIRequest Request being executed. * @param string The username of the user we want to act as */ private function actAsUser( ConduitAPIRequest $api_request, $user_name) { if (!$api_request->getUser()->getIsAdmin()) { throw new Exception("Only administrators can use actAsUser"); } $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user_name); if (!$user) { throw new Exception( "The actAsUser username '{$user_name}' is not a valid user." ); } $api_request->setUser($user); } /** * Authenticate the client making the request to a Phabricator user account. * * @param ConduitAPIRequest Request being executed. * @param dict Request metadata. * @return null|pair Null to indicate successful authentication, or * an error code and error message pair. */ private function authenticateUser( ConduitAPIRequest $api_request, array $metadata) { $request = $this->getRequest(); if ($request->getUser()->getPHID()) { $request->validateCSRF(); return $this->validateAuthenticatedUser( $api_request, $request->getUser()); } // handle oauth $access_token = $request->getStr('access_token'); $method_scope = $metadata['scope']; if ($access_token && $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { $token = id(new PhabricatorOAuthServerAccessToken()) ->loadOneWhere('token = %s', $access_token); if (!$token) { return array( 'ERR-INVALID-AUTH', 'Access token does not exist.', ); } $oauth_server = new PhabricatorOAuthServer(); $valid = $oauth_server->validateAccessToken($token, $method_scope); if (!$valid) { return array( 'ERR-INVALID-AUTH', 'Access token is invalid.', ); } // valid token, so let's log in the user! $user_phid = $token->getUserPHID(); $user = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $user_phid); if (!$user) { return array( 'ERR-INVALID-AUTH', 'Access token is for invalid user.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } // Handle sessionless auth. TOOD: This is super messy. if (isset($metadata['authUser'])) { $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $metadata['authUser']); if (!$user) { return array( 'ERR-INVALID-AUTH', 'Authentication is invalid.', ); } $token = idx($metadata, 'authToken'); $signature = idx($metadata, 'authSignature'); $certificate = $user->getConduitCertificate(); if (sha1($token.$certificate) !== $signature) { return array( 'ERR-INVALID-AUTH', 'Authentication is invalid.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } $session_key = idx($metadata, 'sessionKey'); if (!$session_key) { return array( 'ERR-INVALID-SESSION', 'Session key is not present.' ); } $session = queryfx_one( id(new PhabricatorUser())->establishConnection('r'), 'SELECT * FROM %T WHERE sessionKey = %s', PhabricatorUser::SESSION_TABLE, PhabricatorHash::digest($session_key)); if (!$session) { return array( 'ERR-INVALID-SESSION', 'Session key is invalid.', ); } // TODO: Make sessions timeout. // TODO: When we pull a session, read connectionID from the session table. $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $session['userPHID']); if (!$user) { return array( 'ERR-INVALID-SESSION', 'Session is for nonexistent user.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } private function validateAuthenticatedUser( ConduitAPIRequest $request, PhabricatorUser $user) { if (!$user->isUserActivated()) { return array( 'ERR-USER-DISABLED', pht('User account is not activated.'), ); } $request->setUser($user); return null; } private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, $result = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); if ($request) { foreach ($request->getAllParameters() as $key => $value) { $param_rows[] = array( $key, $this->renderAPIValue($value), ); } } $param_table = new AphrontTableView($param_rows); $param_table->setDeviceReadyTable(true); $param_table->setColumnClasses( array( 'header', 'wide', )); $result_rows = array(); foreach ($result as $key => $value) { $result_rows[] = array( $key, $this->renderAPIValue($value), ); } $result_table = new AphrontTableView($result_rows); $result_table->setDeviceReadyTable(true); $result_table->setColumnClasses( array( 'header', 'wide', )); $param_panel = new AphrontPanelView(); $param_panel->setHeader('Method Parameters'); $param_panel->appendChild($param_table); $result_panel = new AphrontPanelView(); $result_panel->setHeader('Method Result'); $result_panel->appendChild($result_table); $param_head = id(new PHUIHeaderView()) ->setHeader(pht('Method Parameters')); $result_head = id(new PHUIHeaderView()) ->setHeader(pht('Method Result')); $method_uri = $this->getApplicationURI('method/'.$method.'/'); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($method) - ->setHref($method_uri)) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Call'))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($method, $method_uri) + ->addTextCrumb(pht('Call')); return $this->buildApplicationPage( array( $crumbs, $param_head, $param_table, $result_head, $result_table, ), array( 'title' => 'Method Call Result', 'device' => true, )); } private function renderAPIValue($value) { $json = new PhutilJSON(); if (is_array($value)) { $value = $json->encodeFormatted($value); } $value = phutil_tag( 'pre', array('style' => 'white-space: pre-wrap;'), $value); return $value; } private function decodeConduitParams( AphrontRequest $request, $method) { // Look for parameters from the Conduit API Console, which are encoded // as HTTP POST parameters in an array, e.g.: // // params[name]=value¶ms[name2]=value2 // // The fields are individually JSON encoded, since we require users to // enter JSON so that we avoid type ambiguity. $params = $request->getArr('params', null); if ($params !== null) { foreach ($params as $key => $value) { if ($value == '') { // Interpret empty string null (e.g., the user didn't type anything // into the box). $value = 'null'; } $decoded_value = json_decode($value, true); if ($decoded_value === null && strtolower($value) != 'null') { // When json_decode() fails, it returns null. This almost certainly // indicates that a user was using the web UI and didn't put quotes // around a string value. We can either do what we think they meant // (treat it as a string) or fail. For now, err on the side of // caution and fail. In the future, if we make the Conduit API // actually do type checking, it might be reasonable to treat it as // a string if the parameter type is string. throw new Exception( "The value for parameter '{$key}' is not valid JSON. All ". "parameters must be encoded as JSON values, including strings ". "(which means you need to surround them in double quotes). ". "Check your syntax. Value was: {$value}"); } $params[$key] = $decoded_value; } return $params; } // Otherwise, look for a single parameter called 'params' which has the // entire param dictionary JSON encoded. This is the usual case for remote // requests. $params_json = $request->getStr('params'); if (!strlen($params_json)) { if ($request->getBool('allowEmptyParams')) { // TODO: This is a bit messy, but otherwise you can't call // "conduit.ping" from the web console. $params = array(); } else { throw new Exception( "Request has no 'params' key. This may mean that an extension like ". "Suhosin has dropped data from the request. Check the PHP ". "configuration on your server. If you are developing a Conduit ". "client, you MUST provide a 'params' parameter when making a ". "Conduit request, even if the value is empty (e.g., provide '{}')."); } } else { $params = json_decode($params_json, true); if (!is_array($params)) { throw new Exception( "Invalid parameter information was passed to method ". "'{$method}', could not decode JSON serialization. Data: ". $params_json); } } return $params; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index 5efb0d737..82a41de59 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -1,134 +1,132 @@ method = $data['method']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $method = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) ->withMethods(array($this->method)) ->executeOne(); if (!$method) { return new Aphront404Response(); } $status = $method->getMethodStatus(); $reason = $method->getMethodStatusDescription(); $status_view = null; if ($status != ConduitAPIMethod::METHOD_STATUS_STABLE) { $status_view = new AphrontErrorView(); switch ($status) { case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $status_view->setTitle('Deprecated Method'); $status_view->appendChild( nonempty($reason, "This method is deprecated.")); break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $status_view->setSeverity(AphrontErrorView::SEVERITY_WARNING); $status_view->setTitle('Unstable Method'); $status_view->appendChild( nonempty( $reason, "This method is new and unstable. Its interface is subject ". "to change.")); break; } } $error_types = $method->defineErrorTypes(); if ($error_types) { $error_description = array(); foreach ($error_types as $error => $meaning) { $error_description[] = hsprintf( '
  • %s: %s
  • ', $error, $meaning); } $error_description = phutil_tag('ul', array(), $error_description); } else { $error_description = "This method does not raise any specific errors."; } $form = new AphrontFormView(); $form ->setUser($request->getUser()) ->setAction('/api/'.$this->method) ->addHiddenInput('allowEmptyParams', 1) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Description') ->setValue($method->getMethodDescription())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Returns') ->setValue($method->defineReturnType())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Errors') ->setValue($error_description)) ->appendChild(hsprintf( '

    Enter parameters using '. 'JSON. For instance, to enter a list, type: '. '["apple", "banana", "cherry"]')); $params = $method->defineParamTypes(); foreach ($params as $param => $desc) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($param) ->setName("params[{$param}]") ->setCaption($desc)); } $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Output Format') ->setName('output') ->setOptions( array( 'human' => 'Human Readable', 'json' => 'JSON', ))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue('Call Method')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($method->getAPIMethodName()) ->setFormError($status_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($method->getAPIMethodName())); + $crumbs->addTextCrumb($method->getAPIMethodName()); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $method->getAPIMethodName(), 'device' => true, )); } } diff --git a/src/applications/conduit/controller/PhabricatorConduitLogController.php b/src/applications/conduit/controller/PhabricatorConduitLogController.php index ee95ff053..ba384c77f 100644 --- a/src/applications/conduit/controller/PhabricatorConduitLogController.php +++ b/src/applications/conduit/controller/PhabricatorConduitLogController.php @@ -1,135 +1,133 @@ getRequest(); $viewer = $request->getUser(); $conn_table = new PhabricatorConduitConnectionLog(); $call_table = new PhabricatorConduitMethodCallLog(); $conn_r = $call_table->establishConnection('r'); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $pager->setPageSize(500); $query = id(new PhabricatorConduitLogQuery()) ->setViewer($viewer); $methods = $request->getStrList('methods'); if ($methods) { $query->withMethods($methods); } $calls = $query->executeWithCursorPager($pager); $conn_ids = array_filter(mpull($calls, 'getConnectionID')); $conns = array(); if ($conn_ids) { $conns = $conn_table->loadAllWhere( 'id IN (%Ld)', $conn_ids); } $table = $this->renderCallTable($calls, $conns); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Call Logs'))); + $crumbs->addTextCrumb(pht('Call Logs')); return $this->buildApplicationPage( array( $crumbs, $table, $pager, ), array( 'title' => 'Conduit Logs', 'device' => true, )); } private function renderCallTable(array $calls, array $conns) { assert_instances_of($calls, 'PhabricatorConduitMethodCallLog'); assert_instances_of($conns, 'PhabricatorConduitConnectionLog'); $viewer = $this->getRequest()->getUser(); $methods = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) ->execute(); $methods = mpull($methods, null, 'getAPIMethodName'); $rows = array(); foreach ($calls as $call) { $conn = idx($conns, $call->getConnectionID()); if ($conn) { $name = $conn->getUserName(); $client = ' (via '.$conn->getClient().')'; } else { $name = null; $client = null; } $method = idx($methods, $call->getMethod()); if ($method) { switch ($method->getMethodStatus()) { case ConduitAPIMethod::METHOD_STATUS_STABLE: $status = null; break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $status = pht('Unstable'); break; case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $status = pht('Deprecated'); break; } } else { $status = pht('Unknown'); } $rows[] = array( $call->getConnectionID(), $name, array($call->getMethod(), $client), $status, $call->getError(), number_format($call->getDuration()).' us', phabricator_datetime($call->getDateCreated(), $viewer), ); } $table = id(new AphrontTableView($rows)) ->setDeviceReadyTable(true); $table->setHeaders( array( 'Connection', 'User', 'Method', 'Status', 'Error', 'Duration', 'Date', )); $table->setColumnClasses( array( '', '', 'wide', '', '', 'n', 'right', )); return $table; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenController.php b/src/applications/conduit/controller/PhabricatorConduitTokenController.php index b750d60d4..4a44c558c 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenController.php @@ -1,70 +1,68 @@ getRequest()->getUser(); // Ideally we'd like to verify this, but it's fine to leave it unguarded // for now and verifying it would need some Ajax junk or for the user to // click a button or similar. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $old_token = id(new PhabricatorConduitCertificateToken()) ->loadOneWhere( 'userPHID = %s', $user->getPHID()); if ($old_token) { $old_token->delete(); } $token = id(new PhabricatorConduitCertificateToken()) ->setUserPHID($user->getPHID()) ->setToken(Filesystem::readRandomCharacters(40)) ->save(); unset($unguarded); $pre_instructions = pht( 'Copy and paste this token into the prompt given to you by '. '`arc install-certificate`'); $post_instructions = pht( 'After you copy and paste this token, `arc` will complete '. 'the certificate install process for you.'); $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions($pre_instructions) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Token')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($token->getToken())) ->appendRemarkupInstructions($post_instructions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Install Certificate'))); + $crumbs->addTextCrumb(pht('Install Certificate')); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Certificate Token')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => pht('Certificate Install Token'), 'device' => true, )); } } diff --git a/src/applications/config/controller/PhabricatorConfigAllController.php b/src/applications/config/controller/PhabricatorConfigAllController.php index c6a8dba05..d1222775e 100644 --- a/src/applications/config/controller/PhabricatorConfigAllController.php +++ b/src/applications/config/controller/PhabricatorConfigAllController.php @@ -1,141 +1,139 @@ getRequest(); $user = $request->getUser(); $db_values = id(new PhabricatorConfigEntry()) ->loadAllWhere('namespace = %s', 'default'); $db_values = mpull($db_values, null, 'getConfigKey'); $rows = array(); $options = PhabricatorApplicationConfigOptions::loadAllOptions(); ksort($options); foreach ($options as $option) { $key = $option->getKey(); if ($option->getMasked()) { $value = phutil_tag('em', array(), pht('Masked')); } else if ($option->getHidden()) { $value = phutil_tag('em', array(), pht('Hidden')); } else { $value = PhabricatorEnv::getEnvConfig($key); $value = PhabricatorConfigJSON::prettyPrintJSON($value); } $db_value = idx($db_values, $key); $rows[] = array( phutil_tag( 'a', array( 'href' => $this->getApplicationURI('edit/'.$key.'/'), ), $key), $value, $db_value && !$db_value->getIsDeleted() ? pht('Customized') : '', ); } $table = id(new AphrontTableView($rows)) ->setDeviceReadyTable(true) ->setColumnClasses( array( '', 'wide', )) ->setHeaders( array( pht('Key'), pht('Value'), pht('Customized'), )); $title = pht('Current Settings'); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + ->addTextCrumb($title); $panel = new AphrontPanelView(); $panel->appendChild($table); $panel->setNoBackground(); $versions = $this->loadVersions(); $version_property_list = id(new PHUIPropertyListView()); foreach ($versions as $version) { list($name, $hash) = $version; $version_property_list->addProperty($name, $hash); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Settings')) ->addPropertyList($version_property_list); $phabricator_root = dirname(phutil_get_library_root('phabricator')); $version_path = $phabricator_root.'/conf/local/VERSION'; if (Filesystem::pathExists($version_path)) { $version_from_file = Filesystem::readFile($version_path); $version_property_list->addProperty( pht('Local Version'), $version_from_file); } $nav = $this->buildSideNavView(); $nav->selectFilter('all/'); $nav->setCrumbs($crumbs); $nav->appendChild($object_box); $nav->appendChild($panel); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } private function loadVersions() { $specs = array( array( 'name' => pht('Phabricator Version'), 'root' => 'phabricator', ), array( 'name' => pht('Arcanist Version'), 'root' => 'arcanist', ), array( 'name' => pht('libphutil Version'), 'root' => 'phutil', ), ); $futures = array(); foreach ($specs as $key => $spec) { $root = dirname(phutil_get_library_root($spec['root'])); $futures[$key] = id(new ExecFuture('git log --format=%%H -n 1 --')) ->setCWD($root); } $results = array(); foreach ($futures as $key => $future) { list($err, $stdout) = $future->resolve(); if (!$err) { $name = trim($stdout); } else { $name = pht('Unknown'); } $results[$key] = array($specs[$key]['name'], $name); } return array_select_keys($results, array_keys($specs)); } } diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index f7bf9cbb1..42bc8bfe5 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -1,545 +1,536 @@ key = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $options = PhabricatorApplicationConfigOptions::loadAllOptions(); if (empty($options[$this->key])) { $ancient = PhabricatorSetupCheckExtraConfig::getAncientConfig(); if (isset($ancient[$this->key])) { $desc = pht( "This configuration has been removed. You can safely delete ". "it.\n\n%s", $ancient[$this->key]); } else { $desc = pht( "This configuration option is unknown. It may be misspelled, ". "or have existed in a previous version of Phabricator."); } // This may be a dead config entry, which existed in the past but no // longer exists. Allow it to be edited so it can be reviewed and // deleted. $option = id(new PhabricatorConfigOption()) ->setKey($this->key) ->setType('wild') ->setDefault(null) ->setDescription($desc); $group = null; $group_uri = $this->getApplicationURI(); } else { $option = $options[$this->key]; $group = $option->getGroup(); $group_uri = $this->getApplicationURI('group/'.$group->getKey().'/'); } $issue = $request->getStr('issue'); if ($issue) { // If the user came here from an open setup issue, send them back. $done_uri = $this->getApplicationURI('issue/'.$issue.'/'); } else { $done_uri = $group_uri; } // Check if the config key is already stored in the database. // Grab the value if it is. $config_entry = id(new PhabricatorConfigEntry()) ->loadOneWhere( 'configKey = %s AND namespace = %s', $this->key, 'default'); if (!$config_entry) { $config_entry = id(new PhabricatorConfigEntry()) ->setConfigKey($this->key) ->setNamespace('default') ->setIsDeleted(true); $config_entry->setPHID($config_entry->generatePHID()); } $e_value = null; $errors = array(); if ($request->isFormPost() && !$option->getLocked()) { $result = $this->readRequest( $option, $request); list($e_value, $value_errors, $display_value, $xaction) = $result; $errors = array_merge($errors, $value_errors); if (!$errors) { $editor = id(new PhabricatorConfigEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); try { $editor->applyTransactions($config_entry, array($xaction)); return id(new AphrontRedirectResponse())->setURI($done_uri); } catch (PhabricatorConfigValidationException $ex) { $e_value = pht('Invalid'); $errors[] = $ex->getMessage(); } } } else { $display_value = $this->getDisplayValue($option, $config_entry); } $form = new AphrontFormView(); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('You broke everything!')) ->setErrors($errors); } else if ($option->getHidden()) { $msg = pht( "This configuration is hidden and can not be edited or viewed from ". "the web interface."); $error_view = id(new AphrontErrorView()) ->setTitle(pht('Configuration Hidden')) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->appendChild(phutil_tag('p', array(), $msg)); } else if ($option->getLocked()) { $msg = pht( "This configuration is locked and can not be edited from the web ". "interface. Use `./bin/config` in `phabricator/` to edit it."); $error_view = id(new AphrontErrorView()) ->setTitle(pht('Configuration Locked')) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->appendChild(phutil_tag('p', array(), $msg)); } if ($option->getHidden()) { $control = null; } else { $control = $this->renderControl( $option, $display_value, $e_value); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject($option, 'description'); $engine->process(); $description = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($option, 'description')); $form ->setUser($user) ->addHiddenInput('issue', $request->getStr('issue')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Description')) ->setValue($description)); if ($group) { $extra = $group->renderContextualDescription( $option, $request); if ($extra !== null) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setValue($extra)); } } $form ->appendChild($control); $submit_control = id(new AphrontFormSubmitControl()) ->addCancelButton($done_uri); if (!$option->getLocked()) { $submit_control->setValue(pht('Save Config Entry')); } $form->appendChild($submit_control); $examples = $this->renderExamples($option); if ($examples) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Examples')) ->setValue($examples)); } if (!$option->getHidden()) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Default')) ->setValue($this->renderDefaults($option))); } $title = pht('Edit %s', $this->key); $short = pht('Edit'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb(pht('Config'), $this->getApplicationURI()); if ($group) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($group->getName()) - ->setHref($group_uri)); + $crumbs->addTextCrumb($group->getName(), $group_uri); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($this->key) - ->setHref('/config/edit/'.$this->key)); + $crumbs->addTextCrumb($this->key, '/config/edit/'.$this->key); $xactions = id(new PhabricatorConfigTransactionQuery()) ->withObjectPHIDs(array($config_entry->getPHID())) ->setViewer($user) ->execute(); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($config_entry->getPHID()) ->setTransactions($xactions); return $this->buildApplicationPage( array( $crumbs, $form_box, $xaction_view, ), array( 'title' => $title, 'device' => true, )); } private function readRequest( PhabricatorConfigOption $option, AphrontRequest $request) { $xaction = new PhabricatorConfigTransaction(); $xaction->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT); $e_value = null; $errors = array(); $value = $request->getStr('value'); if (!strlen($value)) { $value = null; $xaction->setNewValue( array( 'deleted' => true, 'value' => null, )); return array($e_value, $errors, $value, $xaction); } if ($option->isCustomType()) { $info = $option->getCustomObject()->readRequest($option, $request); list($e_value, $errors, $set_value, $value) = $info; } else { $type = $option->getType(); $set_value = null; switch ($type) { case 'int': if (preg_match('/^-?[0-9]+$/', trim($value))) { $set_value = (int)$value; } else { $e_value = pht('Invalid'); $errors[] = pht('Value must be an integer.'); } break; case 'string': case 'enum': $set_value = (string)$value; break; case 'list': case 'list': $set_value = phutil_split_lines( $request->getStr('value'), $retain_endings = false); foreach ($set_value as $key => $v) { if (!strlen($v)) { unset($set_value[$key]); } } $set_value = array_values($set_value); break; case 'set': $set_value = array_fill_keys($request->getStrList('value'), true); break; case 'bool': switch ($value) { case 'true': $set_value = true; break; case 'false': $set_value = false; break; default: $e_value = pht('Invalid'); $errors[] = pht('Value must be boolean, "true" or "false".'); break; } break; case 'class': if (!class_exists($value)) { $e_value = pht('Invalid'); $errors[] = pht('Class does not exist.'); } else { $base = $option->getBaseClass(); if (!is_subclass_of($value, $base)) { $e_value = pht('Invalid'); $errors[] = pht('Class is not of valid type.'); } else { $set_value = $value; } } break; default: $json = json_decode($value, true); if ($json === null && strtolower($value) != 'null') { $e_value = pht('Invalid'); $errors[] = pht( 'The given value must be valid JSON. This means, among '. 'other things, that you must wrap strings in double-quotes.'); } else { $set_value = $json; } break; } } if (!$errors) { $xaction->setNewValue( array( 'deleted' => false, 'value' => $set_value, )); } else { $xaction = null; } return array($e_value, $errors, $value, $xaction); } private function getDisplayValue( PhabricatorConfigOption $option, PhabricatorConfigEntry $entry) { if ($entry->getIsDeleted()) { return null; } if ($option->isCustomType()) { return $option->getCustomObject()->getDisplayValue($option, $entry); } else { $type = $option->getType(); $value = $entry->getValue(); switch ($type) { case 'int': case 'string': case 'enum': case 'class': return $value; case 'bool': return $value ? 'true' : 'false'; case 'list': case 'list': return implode("\n", nonempty($value, array())); case 'set': return implode("\n", nonempty(array_keys($value), array())); default: return PhabricatorConfigJSON::prettyPrintJSON($value); } } } private function renderControl( PhabricatorConfigOption $option, $display_value, $e_value) { if ($option->isCustomType()) { $control = $option->getCustomObject()->renderControl( $option, $display_value, $e_value); } else { $type = $option->getType(); switch ($type) { case 'int': case 'string': $control = id(new AphrontFormTextControl()); break; case 'bool': $control = id(new AphrontFormSelectControl()) ->setOptions( array( '' => pht('(Use Default)'), 'true' => idx($option->getBoolOptions(), 0), 'false' => idx($option->getBoolOptions(), 1), )); break; case 'enum': $options = array_mergev( array( array('' => pht('(Use Default)')), $option->getEnumOptions(), )); $control = id(new AphrontFormSelectControl()) ->setOptions($options); break; case 'class': $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass($option->getBaseClass()) ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); $names = ipull($symbols, 'name', 'name'); asort($names); $names = array( '' => pht('(Use Default)'), ) + $names; $control = id(new AphrontFormSelectControl()) ->setOptions($names); break; case 'list': case 'list': $control = id(new AphrontFormTextAreaControl()) ->setCaption(pht('Separate values with newlines.')); break; case 'set': $control = id(new AphrontFormTextAreaControl()) ->setCaption(pht('Separate values with newlines or commas.')); break; default: $control = id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setCustomClass('PhabricatorMonospaced') ->setCaption(pht('Enter value in JSON.')); break; } $control ->setLabel(pht('Value')) ->setError($e_value) ->setValue($display_value) ->setName('value'); } if ($option->getLocked()) { $control->setDisabled(true); } return $control; } private function renderExamples(PhabricatorConfigOption $option) { $examples = $option->getExamples(); if (!$examples) { return null; } $table = array(); $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Example')), phutil_tag('th', array(), pht('Value')), )); foreach ($examples as $example) { list($value, $description) = $example; if ($value === null) { $value = phutil_tag('em', array(), pht('(empty)')); } else { if (is_array($value)) { $value = implode("\n", $value); } } $table[] = phutil_tag('tr', array(), array( phutil_tag('th', array(), $description), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } private function renderDefaults(PhabricatorConfigOption $option) { $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $table = array(); $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), pht('Source')), phutil_tag('th', array(), pht('Value')), )); foreach ($stack as $key => $source) { $value = $source->getKeys( array( $option->getKey(), )); if (!array_key_exists($option->getKey(), $value)) { $value = phutil_tag('em', array(), pht('(empty)')); } else { $value = PhabricatorConfigJSON::prettyPrintJSON( $value[$option->getKey()]); } $table[] = phutil_tag('tr', array('class' => 'column-labels'), array( phutil_tag('th', array(), $source->getName()), phutil_tag('td', array(), $value), )); } require_celerity_resource('config-options-css'); return phutil_tag( 'table', array( 'class' => 'config-option-table', ), $table); } } diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php index bdd4c73cd..0f18fb781 100644 --- a/src/applications/config/controller/PhabricatorConfigGroupController.php +++ b/src/applications/config/controller/PhabricatorConfigGroupController.php @@ -1,119 +1,113 @@ groupKey = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $groups = PhabricatorApplicationConfigOptions::loadAll(); $options = idx($groups, $this->groupKey); if (!$options) { return new Aphront404Response(); } $title = pht('%s Configuration', $options->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title); $list = $this->buildOptionList($options->getOptions()); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($options->getName()) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Config'), $this->getApplicationURI()) + ->addTextCrumb($options->getName(), $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $header, $list, ), array( 'title' => $title, 'device' => true, )); } private function buildOptionList(array $options) { assert_instances_of($options, 'PhabricatorConfigOption'); require_celerity_resource('config-options-css'); $db_values = array(); if ($options) { $db_values = id(new PhabricatorConfigEntry())->loadAllWhere( 'configKey IN (%Ls) AND namespace = %s', mpull($options, 'getKey'), 'default'); $db_values = mpull($db_values, null, 'getConfigKey'); } $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getRequest()->getUser()); foreach ($options as $option) { $engine->addObject($option, 'summary'); } $engine->process(); $list = new PHUIObjectItemListView(); $list->setStackable(true); foreach ($options as $option) { $summary = $engine->getOutput($option, 'summary'); $item = id(new PHUIObjectItemView()) ->setHeader($option->getKey()) ->setHref('/config/edit/'.$option->getKey().'/') ->addAttribute($summary); if (!$option->getHidden() && !$option->getMasked()) { $current_value = PhabricatorEnv::getEnvConfig($option->getKey()); $current_value = PhabricatorConfigJSON::prettyPrintJSON( $current_value); $current_value = phutil_tag( 'div', array( 'class' => 'config-options-current-value', ), array( phutil_tag('span', array(), pht('Current Value:')), ' '.$current_value, )); $item->appendChild($current_value); } $db_value = idx($db_values, $option->getKey()); if ($db_value && !$db_value->getIsDeleted()) { $item->addIcon('edit', pht('Customized')); } if ($option->getHidden()) { $item->addIcon('unpublish', pht('Hidden')); } else if ($option->getMasked()) { $item->addIcon('unpublish-grey', pht('Masked')); } else if ($option->getLocked()) { $item->addIcon('lock', pht('Locked')); } $list->addItem($item); } return $list; } } diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index f9b1333ca..7e002c92e 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -1,90 +1,87 @@ getRequest(); $user = $request->getUser(); $nav = $this->buildSideNavView(); $nav->selectFilter('issue/'); $issues = PhabricatorSetupCheck::runAllChecks(); PhabricatorSetupCheck::setOpenSetupIssueCount( PhabricatorSetupCheck::countUnignoredIssues($issues)); $list = $this->buildIssueList($issues); $list->setNoDataString(pht("There are no open setup issues.")); $header = id(new PHUIHeaderView()) ->setHeader(pht('Open Phabricator Setup Issues')); $nav->appendChild( array( $header, $list, )); $title = pht('Setup Issues'); $crumbs = $this ->buildApplicationCrumbs($nav) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup')) - ->setHref($this->getApplicationURI('issue/'))); + ->addTextCrumb(pht('Setup'), $this->getApplicationURI('issue/')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } private function buildIssueList(array $issues) { assert_instances_of($issues, 'PhabricatorSetupIssue'); $list = new PHUIObjectItemListView(); $list->setCards(true); $ignored_items = array(); foreach ($issues as $issue) { $href = $this->getApplicationURI('/issue/'.$issue->getIssueKey().'/'); $item = id(new PHUIObjectItemView()) ->setHeader($issue->getName()) ->setHref($href) ->addAttribute($issue->getSummary()); if (!$issue->getIsIgnored()) { $item->setBarColor('yellow'); $item->addAction( id(new PHUIListItemView()) ->setIcon('unpublish') ->setWorkflow(true) ->setName(pht('Ignore')) ->setHref('/config/ignore/'.$issue->getIssueKey().'/')); $list->addItem($item); } else { $item->addIcon('none', pht('Ignored')); $item->setDisabled(true); $item->addAction( id(new PHUIListItemView()) ->setIcon('preview') ->setWorkflow(true) ->setName(pht('Unignore')) ->setHref('/config/unignore/'.$issue->getIssueKey().'/')); $item->setBarColor('none'); $ignored_items[] = $item; } } foreach ($ignored_items as $item) { $list->addItem($item); } return $list; } } diff --git a/src/applications/config/controller/PhabricatorConfigIssueViewController.php b/src/applications/config/controller/PhabricatorConfigIssueViewController.php index bfc294d7c..d10d154c9 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueViewController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueViewController.php @@ -1,77 +1,71 @@ issueKey = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $issues = PhabricatorSetupCheck::runAllChecks(); PhabricatorSetupCheck::setOpenSetupIssueCount( PhabricatorSetupCheck::countUnignoredIssues($issues)); if (empty($issues[$this->issueKey])) { $content = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Issue Resolved')) ->appendChild(pht('This setup issue has been resolved. ')) ->appendChild( phutil_tag( 'a', array( 'href' => $this->getApplicationURI('issue/'), ), pht('Return to Open Issue List'))); $title = pht('Resolved Issue'); } else { $issue = $issues[$this->issueKey]; $content = $this->renderIssue($issue); $title = $issue->getShortName(); } $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Setup Issues')) - ->setHref($this->getApplicationURI('issue/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + ->addTextCrumb(pht('Setup Issues'), $this->getApplicationURI('issue/')) + ->addTextCrumb($title, $request->getRequestURI()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $title, 'device' => true, )); } private function renderIssue(PhabricatorSetupIssue $issue) { require_celerity_resource('setup-issue-css'); $view = new PhabricatorSetupIssueView(); $view->setIssue($issue); $container = phutil_tag( 'div', array( 'class' => 'setup-issue-background', ), $view->render()); return $container; } } diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index a583fbb49..101e16383 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -1,61 +1,58 @@ getRequest(); $user = $request->getUser(); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $groups = PhabricatorApplicationConfigOptions::loadAll(); $list = $this->buildConfigOptionsList($groups); $title = pht('Phabricator Configuration'); $header = id(new PHUIHeaderView()) ->setHeader($title); $nav->appendChild( array( $header, $list, )); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Config')) - ->setHref($this->getApplicationURI())); + ->addTextCrumb(pht('Config'), $this->getApplicationURI()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } private function buildConfigOptionsList(array $groups) { assert_instances_of($groups, 'PhabricatorApplicationConfigOptions'); $list = new PHUIObjectItemListView(); $list->setStackable(true); $groups = msort($groups, 'getName'); foreach ($groups as $group) { $item = id(new PHUIObjectItemView()) ->setHeader($group->getName()) ->setHref('/config/group/'.$group->getKey().'/') ->addAttribute($group->getDescription()); $list->addItem($item); } return $list; } } diff --git a/src/applications/countdown/controller/PhabricatorCountdownEditController.php b/src/applications/countdown/controller/PhabricatorCountdownEditController.php index 18d7b0bdf..b993f490d 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownEditController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownEditController.php @@ -1,158 +1,151 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $page_title = pht('Edit Countdown'); $countdown = id(new PhabricatorCountdownQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$countdown) { return new Aphront404Response(); } } else { $page_title = pht('Create Countdown'); $countdown = PhabricatorCountdown::initializeNewCountdown($user); } $error_view = null; $e_text = true; $e_epoch = null; if ($request->isFormPost()) { $errors = array(); $title = $request->getStr('title'); $epoch = $request->getStr('epoch'); $view_policy = $request->getStr('viewPolicy'); $e_text = null; if (!strlen($title)) { $e_text = pht('Required'); $errors[] = pht('You must give the countdown a name.'); } if (strlen($epoch)) { $timestamp = PhabricatorTime::parseLocalTime($epoch, $user); if (!$timestamp) { $errors[] = pht( 'You entered an incorrect date. You can enter date '. 'like \'2011-06-26 13:33:37\' to create an event at '. '13:33:37 on the 26th of June 2011.'); } } else { $e_epoch = pht('Required'); $errors[] = pht('You must specify the end date for a countdown.'); } if (!count($errors)) { $countdown->setTitle($title); $countdown->setEpoch($timestamp); $countdown->setViewPolicy($view_policy); $countdown->save(); return id(new AphrontRedirectResponse()) ->setURI('/countdown/'.$countdown->getID().'/'); } else { $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('It\'s not The Final Countdown (du nu nuuu nun)' . ' until you fix these problem')); } } if ($countdown->getEpoch()) { $display_epoch = phabricator_datetime($countdown->getEpoch(), $user); } else { $display_epoch = $request->getStr('epoch'); } $crumbs = $this->buildApplicationCrumbs(); $cancel_uri = '/countdown/'; if ($countdown->getID()) { $cancel_uri = '/countdown/'.$countdown->getID().'/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('C'.$countdown->getID()) - ->setHref($cancel_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb('C'.$countdown->getID(), $cancel_uri); + $crumbs->addTextCrumb(pht('Edit')); $submit_label = pht('Save Changes'); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Countdown'))); + $crumbs->addTextCrumb(pht('Create Countdown')); $submit_label = pht('Create Countdown'); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($countdown) ->execute(); $form = id(new AphrontFormView()) ->setUser($user) ->setAction($request->getRequestURI()->getPath()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($countdown->getTitle()) ->setName('title') ->setError($e_text)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('End Date')) ->setValue($display_epoch) ->setName('epoch') ->setError($e_epoch) ->setCaption(pht('Examples: '. '2011-12-25 or 3 hours or '. 'June 8 2011, 5 PM.'))) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('viewPolicy') ->setPolicyObject($countdown) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_label)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $page_title, 'device' => true, )); } } diff --git a/src/applications/countdown/controller/PhabricatorCountdownViewController.php b/src/applications/countdown/controller/PhabricatorCountdownViewController.php index 1522b4251..7d6ed8c2c 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownViewController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownViewController.php @@ -1,122 +1,120 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $countdown = id(new PhabricatorCountdownQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$countdown) { return new Aphront404Response(); } $countdown_view = id(new PhabricatorCountdownView()) ->setUser($user) ->setCountdown($countdown) ->setHeadless(true); $id = $countdown->getID(); $title = $countdown->getTitle(); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("C{$id}")); + ->addTextCrumb("C{$id}"); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($user) ->setPolicyObject($countdown); $actions = $this->buildActionListView($countdown); $properties = $this->buildPropertyListView($countdown, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $content = array( $crumbs, $object_box, $countdown_view, ); return $this->buildApplicationPage( $content, array( 'title' => $title, 'device' => true, )); } private function buildActionListView(PhabricatorCountdown $countdown) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $countdown->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $countdown, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Countdown')) ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setIcon('delete') ->setName(pht('Delete Countdown')) ->setHref($this->getApplicationURI("delete/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyListView( PhabricatorCountdown $countdown, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $this->loadHandles(array($countdown->getAuthorPHID())); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $view->addProperty( pht('Author'), $this->getHandle($countdown->getAuthorPHID())->renderLink()); return $view; } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index 8a130939d..a7188c5f6 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -1,238 +1,236 @@ getRequest(); $user = $request->getUser(); $window_start = (time() - (60 * 15)); // Assume daemons spend about 250ms second in overhead per task acquiring // leases and doing other bookkeeping. This is probably an over-estimation, // but we'd rather show that utilization is too high than too low. $lease_overhead = 0.250; $completed = id(new PhabricatorWorkerArchiveTask())->loadAllWhere( 'dateModified > %d', $window_start); $failed = id(new PhabricatorWorkerActiveTask())->loadAllWhere( 'failureTime > %d', $window_start); $usage_total = 0; $usage_start = PHP_INT_MAX; $completed_info = array(); foreach ($completed as $completed_task) { $class = $completed_task->getTaskClass(); if (empty($completed_info[$class])) { $completed_info[$class] = array( 'n' => 0, 'duration' => 0, ); } $completed_info[$class]['n']++; $duration = $completed_task->getDuration(); $completed_info[$class]['duration'] += $duration; // NOTE: Duration is in microseconds, but we're just using seconds to // compute utilization. $usage_total += $lease_overhead + ($duration / 1000000); $usage_start = min($usage_start, $completed_task->getDateModified()); } $completed_info = isort($completed_info, 'n'); $rows = array(); foreach ($completed_info as $class => $info) { $rows[] = array( $class, number_format($info['n']), number_format((int)($info['duration'] / $info['n'])).' us', ); } if ($failed) { // Add the time it takes to restart the daemons. This includes a guess // about other overhead of 2X. $usage_total += PhutilDaemonOverseer::RESTART_WAIT * count($failed) * 2; foreach ($failed as $failed_task) { $usage_start = min($usage_start, $failed_task->getFailureTime()); } $rows[] = array( phutil_tag('em', array(), pht('Temporary Failures')), count($failed), null, ); } $logs = id(new PhabricatorDaemonLogQuery()) ->setViewer($user) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); $taskmasters = 0; foreach ($logs as $log) { if ($log->getDaemon() == 'PhabricatorTaskmasterDaemon') { $taskmasters++; } } if ($taskmasters && $usage_total) { // Total number of wall-time seconds the daemons have been running since // the oldest event. For very short times round up to 15s so we don't // render any ridiculous numbers if you reload the page immediately after // restarting the daemons. $available_time = $taskmasters * max(15, (time() - $usage_start)); // Percentage of those wall-time seconds we can account for, which the // daemons spent doing work: $used_time = ($usage_total / $available_time); $rows[] = array( phutil_tag('em', array(), pht('Queue Utilization (Approximate)')), sprintf('%.1f%%', 100 * $used_time), null, ); } $completed_table = new AphrontTableView($rows); $completed_table->setNoDataString( pht('No tasks have completed in the last 15 minutes.')); $completed_table->setHeaders( array( pht('Class'), pht('Count'), pht('Avg'), )); $completed_table->setColumnClasses( array( 'wide', 'n', 'n', )); $completed_header = id(new PHUIHeaderView()) ->setHeader(pht('Recently Completed Tasks (Last 15m)')); $completed_panel = new AphrontPanelView(); $completed_panel->appendChild($completed_table); $completed_panel->setNoBackground(); $daemon_header = id(new PHUIHeaderView()) ->setHeader(pht('Active Daemons')); $daemon_table = new PhabricatorDaemonLogListView(); $daemon_table->setUser($user); $daemon_table->setDaemonLogs($logs); $tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere( 'leaseOwner IS NOT NULL'); $rows = array(); foreach ($tasks as $task) { $rows[] = array( $task->getID(), $task->getTaskClass(), $task->getLeaseOwner(), $task->getLeaseExpires() - time(), $task->getFailureCount(), phutil_tag( 'a', array( 'href' => '/daemon/task/'.$task->getID().'/', 'class' => 'button small grey', ), pht('View Task')), ); } $leased_table = new AphrontTableView($rows); $leased_table->setHeaders( array( pht('ID'), pht('Class'), pht('Owner'), pht('Expires'), pht('Failures'), '', )); $leased_table->setColumnClasses( array( 'n', 'wide', '', '', 'n', 'action', )); $leased_table->setNoDataString(pht('No tasks are leased by workers.')); $leased_panel = new AphrontPanelView(); $leased_panel->setHeader('Leased Tasks'); $leased_panel->appendChild($leased_table); $leased_panel->setNoBackground(); $task_table = new PhabricatorWorkerActiveTask(); $queued = queryfx_all( $task_table->establishConnection('r'), 'SELECT taskClass, count(*) N FROM %T GROUP BY taskClass ORDER BY N DESC', $task_table->getTableName()); $rows = array(); foreach ($queued as $row) { $rows[] = array( $row['taskClass'], number_format($row['N']), ); } $queued_table = new AphrontTableView($rows); $queued_table->setHeaders( array( pht('Class'), pht('Count'), )); $queued_table->setColumnClasses( array( 'wide', 'n', )); $queued_table->setNoDataString(pht('Task queue is empty.')); $queued_panel = new AphrontPanelView(); $queued_panel->setHeader(pht('Queued Tasks')); $queued_panel->appendChild($queued_table); $queued_panel->setNoBackground(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Console'))); + $crumbs->addTextCrumb(pht('Console')); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $nav->appendChild( array( $crumbs, $completed_header, $completed_panel, $daemon_header, $daemon_table, $queued_panel, $leased_panel, )); return $this->buildApplicationPage( $nav, array( 'title' => pht('Console'), )); } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php index a57b41c48..aaf5e00e8 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php @@ -1,52 +1,49 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $event = id(new PhabricatorDaemonLogEvent())->load($this->id); if (!$event) { return new Aphront404Response(); } $event_view = id(new PhabricatorDaemonLogEventsView()) ->setEvents(array($event)) ->setUser($request->getUser()) ->setCombinedLog(true) ->setShowFullMessage(true); $log_panel = new AphrontPanelView(); $log_panel->appendChild($event_view); $log_panel->setNoBackground(); $daemon_id = $event->getLogID(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Daemon %s', $daemon_id)) - ->setHref($this->getApplicationURI("log/{$daemon_id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Event %s', $event->getID()))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb( + pht('Daemon %s', $daemon_id), + $this->getApplicationURI("log/{$daemon_id}/")) + ->addTextCrumb(pht('Event %s', $event->getID())); return $this->buildApplicationPage( array( $crumbs, $log_panel, ), array( 'title' => pht('Combined Daemon Log'), )); } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php index 32aa288f4..e97220a24 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php @@ -1,40 +1,38 @@ getRequest(); $viewer = $request->getUser(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $logs = id(new PhabricatorDaemonLogQuery()) ->setViewer($viewer) ->executeWithCursorPager($pager); $daemon_table = new PhabricatorDaemonLogListView(); $daemon_table->setUser($request->getUser()); $daemon_table->setDaemonLogs($logs); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('All Daemons'))); + $crumbs->addTextCrumb(pht('All Daemons')); $nav = $this->buildSideNavView(); $nav->selectFilter('log'); $nav->setCrumbs($crumbs); $nav->appendChild($daemon_table); $nav->appendChild($pager); return $this->buildApplicationPage( $nav, array( 'title' => pht('All Daemons'), 'device' => true, )); } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index 12fd28368..acec799b5 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -1,183 +1,181 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $log = id(new PhabricatorDaemonLogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$log) { return new Aphront404Response(); } $events = id(new PhabricatorDaemonLogEvent())->loadAllWhere( 'logID = %d ORDER BY id DESC LIMIT 1000', $log->getID()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Daemon %s', $log->getID()))); + $crumbs->addTextCrumb(pht('Daemon %s', $log->getID())); $header = id(new PHUIHeaderView()) ->setHeader($log->getDaemon()); $tag = id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE); $status = $log->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_UNKNOWN: $tag->setBackgroundColor(PhabricatorTagView::COLOR_ORANGE); $tag->setName(pht('Unknown')); break; case PhabricatorDaemonLog::STATUS_RUNNING: $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREEN); $tag->setName(pht('Running')); break; case PhabricatorDaemonLog::STATUS_DEAD: $tag->setBackgroundColor(PhabricatorTagView::COLOR_RED); $tag->setName(pht('Dead')); break; case PhabricatorDaemonLog::STATUS_WAIT: $tag->setBackgroundColor(PhabricatorTagView::COLOR_BLUE); $tag->setName(pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREY); $tag->setName(pht('Exited')); break; } $header->addTag($tag); $properties = $this->buildPropertyListView($log); $event_view = id(new PhabricatorDaemonLogEventsView()) ->setUser($user) ->setEvents($events); $event_panel = new AphrontPanelView(); $event_panel->setNoBackground(); $event_panel->appendChild($event_view); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $event_panel, ), array( 'title' => pht('Daemon Log'), )); } private function buildPropertyListView(PhabricatorDaemonLog $daemon) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $id = $daemon->getID(); $c_epoch = $daemon->getDateCreated(); $u_epoch = $daemon->getDateModified(); $unknown_time = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); $dead_time = PhabricatorDaemonLogQuery::getTimeUntilDead(); $wait_time = PhutilDaemonOverseer::RESTART_WAIT; $details = null; $status = $daemon->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_RUNNING: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s).', phabricator_format_relative_time($unknown_time)); break; case PhabricatorDaemonLog::STATUS_UNKNOWN: $details = pht( 'This daemon has not reported a status update recently (within %s). '. 'It may have exited abruptly. After %s, it will be presumed dead.', phabricator_format_relative_time($unknown_time), phabricator_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_DEAD: $details = pht( 'This daemon did not report a status update for %s. It is '. 'presumed dead. Usually, this indicates that the daemon was '. 'killed or otherwise exited abruptly with an error. You may '. 'need to restart it.', phabricator_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_WAIT: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s). However, it encountered an error while '. 'doing work and is waiting a little while (%s) to resume '. 'processing. After encountering an error, daemons wait before '. 'resuming work to avoid overloading services.', phabricator_format_relative_time($unknown_time), phabricator_format_relative_time($wait_time)); break; case PhabricatorDaemonLog::STATUS_EXITED: $details = pht( 'This daemon exited normally and is no longer running.'); break; } $view->addProperty(pht('Status Details'), $details); $view->addProperty(pht('Daemon Class'), $daemon->getDaemon()); $view->addProperty(pht('Host'), $daemon->getHost()); $view->addProperty(pht('PID'), $daemon->getPID()); $view->addProperty(pht('Started'), phabricator_datetime($c_epoch, $viewer)); $view->addProperty( pht('Seen'), pht( '%s ago (%s)', phabricator_format_relative_time(time() - $u_epoch), phabricator_datetime($u_epoch, $viewer))); $argv = $daemon->getArgv(); if (is_array($argv)) { $argv = implode("\n", $argv); } $view->addProperty( pht('Argv'), phutil_tag( 'textarea', array( 'style' => 'width: 100%; height: 12em;', ), $argv)); $view->addProperty( pht('View Full Logs'), phutil_tag( 'tt', array(), "phabricator/ $ ./bin/phd log {$id}")); return $view; } } diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php index f8f9b39f7..68b75ba06 100644 --- a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php +++ b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php @@ -1,264 +1,262 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $task = id(new PhabricatorWorkerActiveTask())->load($this->id); if (!$task) { $task = id(new PhabricatorWorkerArchiveTask())->load($this->id); } if (!$task) { $title = pht('Task Does Not Exist'); $error_view = new AphrontErrorView(); $error_view->setTitle(pht('No Such Task')); $error_view->appendChild(phutil_tag( 'p', array(), pht('This task may have recently been garbage collected.'))); $error_view->setSeverity(AphrontErrorView::SEVERITY_NODATA); $content = $error_view; } else { $title = pht('Task %d', $task->getID()); $header = id(new PHUIHeaderView()) ->setHeader(pht('Task %d (%s)', $task->getID(), $task->getTaskClass())); $actions = $this->buildActionListView($task); $properties = $this->buildPropertyListView($task, $actions); $retry_head = id(new PHUIHeaderView()) ->setHeader(pht('Retries')); $retry_info = $this->buildRetryListView($task); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $content = array( $object_box, $retry_head, $retry_info, ); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $title, 'device' => true, )); } private function buildActionListView(PhabricatorWorkerTask $task) { $request = $this->getRequest(); $user = $request->getUser(); $id = $task->getID(); $view = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()); if ($task->isArchived()) { $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS; $can_retry = ($task->getResult() != $result_success); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Retry Task')) ->setHref($this->getApplicationURI('/task/'.$id.'/retry/')) ->setIcon('undo') ->setWorkflow(true) ->setDisabled(!$can_retry)); } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Cancel Task')) ->setHref($this->getApplicationURI('/task/'.$id.'/cancel/')) ->setIcon('delete') ->setWorkflow(true)); } $can_release = (!$task->isArchived()) && ($task->getLeaseOwner()); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Free Lease')) ->setHref($this->getApplicationURI('/task/'.$id.'/release/')) ->setIcon('unlock') ->setWorkflow(true) ->setDisabled(!$can_release)); return $view; } private function buildPropertyListView( PhabricatorWorkerTask $task, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); if ($task->isArchived()) { switch ($task->getResult()) { case PhabricatorWorkerArchiveTask::RESULT_SUCCESS: $status = pht('Complete'); break; case PhabricatorWorkerArchiveTask::RESULT_FAILURE: $status = pht('Failed'); break; case PhabricatorWorkerArchiveTask::RESULT_CANCELLED: $status = pht('Cancelled'); break; default: throw new Exception("Unknown task status!"); } } else { $status = pht('Queued'); } $view->addProperty( pht('Task Status'), $status); $view->addProperty( pht('Task Class'), $task->getTaskClass()); if ($task->getLeaseExpires()) { if ($task->getLeaseExpires() > time()) { $lease_status = pht('Leased'); } else { $lease_status = pht('Lease Expired'); } } else { $lease_status = phutil_tag('em', array(), pht('Not Leased')); } $view->addProperty( pht('Lease Status'), $lease_status); $view->addProperty( pht('Lease Owner'), $task->getLeaseOwner() ? $task->getLeaseOwner() : phutil_tag('em', array(), pht('None'))); if ($task->getLeaseExpires() && $task->getLeaseOwner()) { $expires = ($task->getLeaseExpires() - time()); $expires = phabricator_format_relative_time_detailed($expires); } else { $expires = phutil_tag('em', array(), pht('None')); } $view->addProperty( pht('Lease Expires'), $expires); if ($task->isArchived()) { $duration = number_format($task->getDuration()).' us'; } else { $duration = phutil_tag('em', array(), pht('Not Completed')); } $view->addProperty( pht('Duration'), $duration); $data = id(new PhabricatorWorkerTaskData())->load($task->getDataID()); $task->setData($data->getData()); $worker = $task->getWorkerInstance(); $data = $worker->renderForDisplay(); $view->addProperty( pht('Data'), $data); return $view; } private function buildRetryListView(PhabricatorWorkerTask $task) { $view = new PHUIPropertyListView(); $data = id(new PhabricatorWorkerTaskData())->load($task->getDataID()); $task->setData($data->getData()); $worker = $task->getWorkerInstance(); $view->addProperty( pht('Failure Count'), $task->getFailureCount()); $retry_count = $worker->getMaximumRetryCount(); if ($retry_count === null) { $max_retries = phutil_tag('em', array(), pht('Retries Forever')); $retry_count = INF; } else { $max_retries = $retry_count; } $view->addProperty( pht('Maximum Retries'), $max_retries); $projection = clone $task; $projection->makeEphemeral(); $next = array(); for ($ii = $task->getFailureCount(); $ii < $retry_count; $ii++) { $projection->setFailureCount($ii); $next[] = $worker->getWaitBeforeRetry($projection); if (count($next) > 10) { break; } } if ($next) { $cumulative = 0; foreach ($next as $key => $duration) { if ($duration === null) { $duration = 60; } $cumulative += $duration; $next[$key] = phabricator_format_relative_time($cumulative); } if ($ii != $retry_count) { $next[] = '...'; } $retries_in = implode(', ', $next); } else { $retries_in = pht('No More Retries'); } $view->addProperty( pht('Retries After'), $retries_in); return $view; } } diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index dd2776b42..cd3048711 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -1,111 +1,109 @@ getRequest(); $errors = array(); $e_diff = null; $e_file = null; if ($request->isFormPost()) { $diff = null; if ($request->getFileExists('diff-file')) { $diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']); } else { $diff = $request->getStr('diff'); } if (!strlen($diff)) { $errors[] = pht( "You can not create an empty diff. Copy/paste a diff, or upload a ". "diff file."); $e_diff = pht('Required'); $e_file = pht('Required'); } if (!$errors) { $call = new ConduitCall( 'differential.createrawdiff', array( 'diff' => $diff, )); $call->setUser($request->getUser()); $result = $call->execute(); $path = id(new PhutilURI($result['uri']))->getPath(); return id(new AphrontRedirectResponse())->setURI($path); } } $form = new AphrontFormView(); $arcanist_href = PhabricatorEnv::getDoclink( 'article/Arcanist_User_Guide.html'); $arcanist_link = phutil_tag( 'a', array( 'href' => $arcanist_href, 'target' => '_blank', ), 'Arcanist'); $cancel_uri = $this->getApplicationURI(); $form ->setAction('/differential/diff/create/') ->setEncType('multipart/form-data') ->setUser($request->getUser()) ->appendInstructions( pht( 'The best way to create a Differential diff is by using %s, but you '. 'can also just paste a diff (for example, from %s, %s or %s) into '. 'this box, or upload a diff file.', $arcanist_link, phutil_tag('tt', array(), 'svn diff'), phutil_tag('tt', array(), 'git diff'), phutil_tag('tt', array(), 'hg diff --git'))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Raw Diff')) ->setName('diff') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setError($e_diff)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('Raw Diff From File')) ->setName('diff-file') ->setError($e_file)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht("Create Diff"))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Diff')) ->setFormError($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Diff'))); + $crumbs->addTextCrumb(pht('Create Diff')); if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create Diff'), 'device' => true, )); } } diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php index f6543a93d..1063b5338 100644 --- a/src/applications/differential/controller/DifferentialDiffViewController.php +++ b/src/applications/differential/controller/DifferentialDiffViewController.php @@ -1,167 +1,165 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$diff) { return new Aphront404Response(); } if ($diff->getRevisionID()) { $top_part = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->appendChild( pht( 'This diff belongs to revision %s.', phutil_tag( 'a', array( 'href' => '/D'.$diff->getRevisionID(), ), 'D'.$diff->getRevisionID()))); } else { // TODO: implement optgroup support in AphrontFormSelectControl? $select = array(); $select[] = hsprintf('', pht('Create New Revision')); $select[] = phutil_tag( 'option', array('value' => ''), pht('Create a new Revision...')); $select[] = hsprintf(''); $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withAuthors(array($viewer->getPHID())) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->execute(); if ($revisions) { $select[] = hsprintf( '', pht('Update Existing Revision')); foreach ($revisions as $revision) { $select[] = phutil_tag( 'option', array( 'value' => $revision->getID(), ), 'D'.$revision->getID().' '.$revision->getTitle()); } $select[] = hsprintf(''); } $select = phutil_tag( 'select', array('name' => 'revisionID'), $select); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->setAction('/differential/revision/edit/') ->addHiddenInput('diffID', $diff->getID()) ->addHiddenInput('viaDiffView', 1) ->appendRemarkupInstructions( pht( 'Review the diff for correctness. When you are satisfied, either '. '**create a new revision** or **update an existing revision**.')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Attach To')) ->setValue($select)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue'))); $top_part = $form; } $props = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $diff->getID()); $props = mpull($props, 'getData', 'getName'); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnDiffView()) { unset($aux_fields[$key]); } else { $aux_field->setUser($this->getRequest()->getUser()); } } $dict = array(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setDiff($diff); $aux_field->setManualDiff($diff); $aux_field->setDiffProperties($props); $value = $aux_field->renderValueForDiffView(); if (strlen($value)) { $label = rtrim($aux_field->renderLabelForDiffView(), ':'); $dict[$label] = $value; } } $property_head = id(new PHUIHeaderView()) ->setHeader(pht('Properties')); $property_view = new PHUIPropertyListView(); foreach ($dict as $key => $value) { $property_view->addProperty($key, $value); } $changesets = $diff->loadChangesets(); $changesets = msort($changesets, 'getSortKey'); $table_of_contents = id(new DifferentialDiffTableOfContentsView()) ->setChangesets($changesets) ->setVisibleChangesets($changesets) ->setUnitTestData(idx($props, 'arc:unit', array())); $refs = array(); foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } $details = id(new DifferentialChangesetListView()) ->setChangesets($changesets) ->setVisibleChangesets($changesets) ->setRenderingReferences($refs) ->setStandaloneURI('/differential/changeset/') ->setDiff($diff) ->setTitle(pht('Diff %d', $diff->getID())) ->setUser($request->getUser()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Diff %d', $diff->getID()))); + $crumbs->addTextCrumb(pht('Diff %d', $diff->getID())); return $this->buildApplicationPage( array( $crumbs, $top_part, $property_head, $property_view, $table_of_contents, $details, ), array( 'title' => pht('Diff View'), )); } } diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index a136274de..e58c88038 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -1,211 +1,207 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if (!$this->id) { $this->id = $request->getInt('revisionID'); } if ($this->id) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needRelationships(true) ->needReviewerStatus(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$revision) { return new Aphront404Response(); } } else { $revision = DifferentialRevision::initializeNewRevision($viewer); } $aux_fields = $this->loadAuxiliaryFields($revision); $diff_id = $request->getInt('diffID'); if ($diff_id) { $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withIDs(array($diff_id)) ->executeOne(); if (!$diff) { return new Aphront404Response(); } if ($diff->getRevisionID()) { // TODO: Redirect? throw new Exception("This diff is already attached to a revision!"); } } else { $diff = null; } $errors = array(); if ($request->isFormPost() && !$request->getStr('viaDiffView')) { foreach ($aux_fields as $aux_field) { $aux_field->setValueFromRequest($request); try { $aux_field->validateField(); } catch (DifferentialFieldValidationException $ex) { $errors[] = $ex->getMessage(); } } if (!$errors) { $is_new = !$revision->getID(); $user = $request->getUser(); $editor = new DifferentialRevisionEditor($revision); $editor->setActor($request->getUser()); if ($diff) { $editor->addDiff($diff, $request->getStr('comments')); } $editor->setAuxiliaryFields($aux_fields); $editor->setAphrontRequestForEventDispatch($request); $editor->save(); return id(new AphrontRedirectResponse()) ->setURI('/D'.$revision->getID()); } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionEdit(); } $phids = array_mergev($aux_phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); foreach ($aux_fields as $key => $aux_field) { $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($diff) { $form->addHiddenInput('diffID', $diff->getID()); } if ($revision->getID()) { $form->setAction('/differential/revision/edit/'.$revision->getID().'/'); } else { $form->setAction('/differential/revision/edit/'); } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } if ($diff && $revision->getID()) { $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Comments')) ->setName('comments') ->setCaption(pht("Explain what's new in this diff.")) ->setValue($request->getStr('comments'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))) ->appendChild( id(new AphrontFormDividerControl())); } $preview = array(); foreach ($aux_fields as $aux_field) { $control = $aux_field->renderEditControl(); if ($control) { $form->appendChild($control); } $preview[] = $aux_field->renderEditPreview(); } $submit = id(new AphrontFormSubmitControl()) ->setValue('Save'); if ($diff) { $submit->addCancelButton('/differential/diff/'.$diff->getID().'/'); } else { $submit->addCancelButton('/D'.$revision->getID()); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($revision->getID()) { if ($diff) { $title = pht('Update Differential Revision'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('D'.$revision->getID()) - ->setHref('/differential/diff/'.$diff->getID().'/')); + $crumbs->addTextCrumb( + 'D'.$revision->getID(), + '/differential/diff/'.$diff->getID().'/'); } else { $title = pht('Edit Differential Revision'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('D'.$revision->getID()) - ->setHref('/D'.$revision->getID())); + $crumbs->addTextCrumb( + 'D'.$revision->getID(), + '/D'.$revision->getID()); } } else { $title = pht('Create New Differential Revision'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview), array( 'title' => $title, 'device' => true, )); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setRevision($revision); if (!$aux_field->shouldAppearOnEdit()) { unset($aux_fields[$key]); } else { $aux_field->setUser($user); } } return DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); } } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index e418669ad..60a0acf21 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,921 +1,918 @@ revisionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $viewer_is_anonymous = !$user->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($request->getUser()) ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( "This revision has no diffs. Something has gone quite wrong."); } $diff_vs = $request->getInt('vs'); $target_id = $request->getInt('id'); $target = idx($diffs, $target_id, end($diffs)); $target_manual = $target; if (!$target_id) { foreach ($diffs as $diff) { if ($diff->getCreationMethod() != 'commit') { $target_manual = $diff; } } } if (empty($diffs[$diff_vs])) { $diff_vs = null; } $arc_project = $target->loadArcanistProject(); $repository = ($arc_project ? $arc_project->loadRepository() : null); list($changesets, $vs_map, $vs_changesets, $rendering_references) = $this->loadChangesetsAndVsMap( $target, idx($diffs, $diff_vs), $repository); if ($request->getExists('download')) { return $this->buildRawDiffResponse( $changesets, $vs_changesets, $vs_map, $repository); } $props = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $target_manual->getID()); $props = mpull($props, 'getData', 'getName'); $aux_fields = $this->loadAuxiliaryFields($revision); $comments = $revision->loadComments(); $all_changesets = $changesets; $inlines = $this->loadInlineComments( $revision, $all_changesets); $object_phids = array_merge( $revision->getReviewers(), $revision->getCCPHIDs(), $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $user->getPHID(), ), mpull($comments, 'getAuthorPHID')); foreach ($comments as $comment) { foreach ($comment->getRequiredHandlePHIDs() as $phid) { $object_phids[] = $phid; } } foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setDiff($target); $aux_field->setManualDiff($target_manual); $aux_field->setDiffProperties($props); $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView(); } $object_phids = array_merge($object_phids, array_mergev($aux_phids)); $object_phids = array_unique($object_phids); $handles = $this->loadViewerHandles($object_phids); foreach ($aux_fields as $key => $aux_field) { // Make sure each field only has access to handles it specifically // requested, not all handles. Otherwise you can get a field which works // only in the presence of other fields. $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $reviewer_warning = null; if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { $has_live_reviewer = false; foreach ($revision->getReviewers() as $reviewer) { if (!$handles[$reviewer]->isDisabled()) { $has_live_reviewer = true; break; } } if (!$has_live_reviewer) { $reviewer_warning = new AphrontErrorView(); $reviewer_warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $reviewer_warning->setTitle(pht('No Active Reviewers')); if ($revision->getReviewers()) { $reviewer_warning->appendChild( phutil_tag( 'p', array(), pht('All specified reviewers are disabled and this revision '. 'needs review. You may want to add some new reviewers.'))); } else { $reviewer_warning->appendChild( phutil_tag( 'p', array(), pht('This revision has no specified reviewers and needs '. 'review. You may want to add some reviewers.'))); } } } $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new AphrontErrorView(); $warning->setTitle('Very Large Diff'); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->appendChild(hsprintf( '%s %s', pht( 'This diff is very large and affects %s files. Load each file '. 'individually.', new PhutilNumber($count)), phutil_tag( 'a', array( 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')))); $warning = $warning->render(); $my_inlines = id(new DifferentialInlineCommentQuery()) ->withDraftComments($user->getPHID(), $this->revisionID) ->execute(); $visible_changesets = array(); foreach ($inlines + $my_inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } if (!empty($props['arc:lint'])) { $changeset_paths = mpull($changesets, null, 'getFilename'); foreach ($props['arc:lint'] as $lint) { $changeset = idx($changeset_paths, $lint['path']); if ($changeset) { $visible_changesets[$changeset->getID()] = $changeset; } } } } else { $warning = null; $visible_changesets = $changesets; } $revision_detail = id(new DifferentialRevisionDetailView()) ->setUser($user) ->setRevision($revision) ->setDiff(end($diffs)) ->setAuxiliaryFields($aux_fields) ->setURI($request->getRequestURI()); $actions = $this->getRevisionActions($revision); $custom_renderer_class = PhabricatorEnv::getEnvConfig( 'differential.revision-custom-detail-renderer'); if ($custom_renderer_class) { // TODO: build a better version of the action links and deprecate the // whole DifferentialRevisionDetailRenderer class. $custom_renderer = newv($custom_renderer_class, array()); $custom_renderer->setUser($user); $custom_renderer->setDiff($target); if ($diff_vs) { $custom_renderer->setVSDiff($diffs[$diff_vs]); } $actions = array_merge( $actions, $custom_renderer->generateActionLinks($revision, $target_manual)); } $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL); if ($arc_project) { list($symbol_indexes, $project_phids) = $this->buildSymbolIndexes( $arc_project, $visible_changesets); } else { $symbol_indexes = array(); $project_phids = null; } $revision_detail->setActions($actions); $revision_detail->setUser($user); $comment_view = new DifferentialRevisionCommentListView(); $comment_view->setComments($comments); $comment_view->setHandles($handles); $comment_view->setInlineComments($inlines); $comment_view->setChangesets($all_changesets); $comment_view->setUser($user); $comment_view->setTargetDiff($target); $comment_view->setVersusDiffID($diff_vs); if ($arc_project) { Javelin::initBehavior( 'repository-crossreference', array( 'section' => $comment_view->getID(), 'projects' => $project_phids, )); } $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($changesets); $changeset_view->setVisibleChangesets($visible_changesets); if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $changeset_view->setStandaloneURI('/differential/changeset/'); $changeset_view->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new'); $changeset_view->setUser($user); $changeset_view->setDiff($target); $changeset_view->setRenderingReferences($rendering_references); $changeset_view->setVsMap($vs_map); $changeset_view->setWhitespace($whitespace); if ($repository) { $changeset_view->setRepository($repository); } $changeset_view->setSymbolIndexes($symbol_indexes); $changeset_view->setTitle('Diff '.$target->getID()); $diff_history = new DifferentialRevisionUpdateHistoryView(); $diff_history->setDiffs($diffs); $diff_history->setSelectedVersusDiffID($diff_vs); $diff_history->setSelectedDiffID($target->getID()); $diff_history->setSelectedWhitespace($whitespace); $diff_history->setUser($user); $local_view = new DifferentialLocalCommitsView(); $local_view->setUser($user); $local_view->setLocalCommits(idx($props, 'local:commits')); if ($repository) { $other_revisions = $this->loadOtherRevisions( $changesets, $target, $repository); } else { $other_revisions = array(); } $other_view = null; if ($other_revisions) { $other_view = $this->renderOtherRevisions($other_revisions); } $toc_view = new DifferentialDiffTableOfContentsView(); $toc_view->setChangesets($changesets); $toc_view->setVisibleChangesets($visible_changesets); $toc_view->setRenderingReferences($rendering_references); $toc_view->setUnitTestData(idx($props, 'arc:unit', array())); if ($repository) { $toc_view->setRepository($repository); } $toc_view->setDiff($target); $toc_view->setUser($user); $toc_view->setRevisionID($revision->getID()); $toc_view->setWhitespace($whitespace); $comment_form = null; if (!$viewer_is_anonymous) { $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-'.$revision->getID()); $reviewers = array(); $ccs = array(); if ($draft) { $reviewers = idx($draft->getMetadata(), 'reviewers', array()); $ccs = idx($draft->getMetadata(), 'ccs', array()); if ($reviewers || $ccs) { $handles = $this->loadViewerHandles(array_merge($reviewers, $ccs)); $reviewers = array_select_keys($handles, $reviewers); $ccs = array_select_keys($handles, $ccs); } } $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); $comment_form->setAuxFields($aux_fields); $comment_form->setActions($this->getRevisionCommentActions($revision)); $comment_form->setActionURI('/differential/comment/save/'); $comment_form->setUser($user); $comment_form->setDraft($draft); $comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID')); $comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID')); // TODO: This just makes the "Z" key work. Generalize this and remove // it at some point. $comment_form = phutil_tag( 'div', array( 'class' => 'differential-add-comment-panel', ), $comment_form); } $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); Javelin::initBehavior('differential-user-select'); $page_pane = id(new DifferentialPrimaryPaneView()) ->setID($pane_id) ->appendChild(array( $comment_view, $diff_history, $warning, $local_view, $toc_view, $other_view, $changeset_view, )); if ($comment_form) { $page_pane->appendChild($comment_form); } else { // TODO: For now, just use this to get "Login to Comment". $page_pane->appendChild( id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI())); } $object_id = 'D'.$revision->getID(); $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $content = array( $reviewer_warning, $top_anchor, $revision_detail, $page_pane, ); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($object_id) - ->setHref('/'.$object_id)); + $crumbs->addTextCrumb($object_id, '/'.$object_id); $prefs = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; if ($prefs->getPreference($pref_filetree)) { $collapsed = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED, false); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle('D'.$revision->getID()) ->setBaseURI(new PhutilURI('/D'.$revision->getID())) ->setCollapsed((bool)$collapsed) ->build($changesets); $nav->appendChild($content); $nav->setCrumbs($crumbs); $content = $nav; } else { array_unshift($content, $crumbs); } return $this->buildApplicationPage( $content, array( 'title' => $object_id.' '.$revision->getTitle(), 'pageObjects' => array($revision->getPHID()), )); } private function getRevisionActions(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $viewer_phid = $user->getPHID(); $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); $logged_in = $this->getRequest()->getUser()->isLoggedIn(); $status = $revision->getStatus(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $links = array(); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $revision, PhabricatorPolicyCapability::CAN_EDIT); $links[] = array( 'icon' => 'edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => pht('Edit Revision'), 'disabled' => !$can_edit, 'sigil' => $can_edit ? null : 'workflow', ); if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( 'icon' => $viewer_is_cc ? 'disable' : 'check', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'), 'instant' => $logged_in, 'disabled' => !$logged_in, 'sigil' => $can_edit ? null : 'workflow', ); } else { $links[] = array( 'icon' => 'enable', 'name' => pht('Automatically Subscribed'), 'disabled' => true, ); } require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); $links[] = array( 'icon' => 'link', 'name' => pht('Edit Dependencies'), 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/", 'sigil' => 'workflow', 'disabled' => !$can_edit, ); $maniphest = 'PhabricatorApplicationManiphest'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $links[] = array( 'icon' => 'attach', 'name' => pht('Edit Maniphest Tasks'), 'href' => "/search/attach/{$revision_phid}/TASK/", 'sigil' => 'workflow', 'disabled' => !$can_edit, ); } $request_uri = $this->getRequest()->getRequestURI(); $links[] = array( 'icon' => 'download', 'name' => pht('Download Raw Diff'), 'href' => $request_uri->alter('download', 'true') ); return $links; } private function getRevisionCommentActions(DifferentialRevision $revision) { $actions = array( DifferentialAction::ACTION_COMMENT => true, ); $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID()); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $status = $revision->getStatus(); $viewer_has_accepted = false; $viewer_has_rejected = false; $status_accepted = DifferentialReviewerStatus::STATUS_ACCEPTED; $status_rejected = DifferentialReviewerStatus::STATUS_REJECTED; foreach ($revision->getReviewerStatus() as $reviewer) { if ($reviewer->getReviewerPHID() == $viewer_phid) { if ($reviewer->getStatus() == $status_accepted) { $viewer_has_accepted = true; } if ($reviewer->getStatus() == $status_rejected) { $viewer_has_rejected = true; } break; } } $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept'); $always_allow_close = PhabricatorEnv::getEnvConfig( 'differential.always-allow-close'); $allow_reopen = PhabricatorEnv::getEnvConfig( 'differential.allow-reopen'); if ($viewer_is_owner) { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept; $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; $actions[DifferentialAction::ACTION_RETHINK] = true; $actions[DifferentialAction::ACTION_CLOSE] = true; break; case ArcanistDifferentialRevisionStatus::CLOSED: break; case ArcanistDifferentialRevisionStatus::ABANDONED: $actions[DifferentialAction::ACTION_RECLAIM] = true; break; } } else { switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = !$viewer_has_rejected; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ACCEPT] = !$viewer_has_accepted; $actions[DifferentialAction::ACTION_REJECT] = true; $actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer; break; case ArcanistDifferentialRevisionStatus::CLOSED: case ArcanistDifferentialRevisionStatus::ABANDONED: break; } if ($status != ArcanistDifferentialRevisionStatus::CLOSED) { $actions[DifferentialAction::ACTION_CLAIM] = true; $actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close; } } $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true; $actions[DifferentialAction::ACTION_ADDCCS] = true; $actions[DifferentialAction::ACTION_REOPEN] = $allow_reopen && ($status == ArcanistDifferentialRevisionStatus::CLOSED); $actions = array_keys(array_filter($actions)); $actions_dict = array(); foreach ($actions as $action) { $actions_dict[$action] = DifferentialAction::getActionVerb($action); } return $actions_dict; } private function loadInlineComments( DifferentialRevision $revision, array &$changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $inline_comments = array(); $inline_comments = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($revision->getID())) ->withNotDraft(true) ->execute(); $load_changesets = array(); foreach ($inline_comments as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { continue; } $load_changesets[$changeset_id] = true; } $more_changesets = array(); if ($load_changesets) { $changeset_ids = array_keys($load_changesets); $more_changesets += id(new DifferentialChangeset()) ->loadAllWhere( 'id IN (%Ld)', $changeset_ids); } if ($more_changesets) { $changesets += $more_changesets; $changesets = msort($changesets, 'getSortKey'); } return $inline_comments; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_ids = array(); if ($diff_vs) { $load_ids[] = $diff_vs->getID(); } $load_ids[] = $target->getID(); $raw_changesets = id(new DifferentialChangeset()) ->loadAllWhere( 'diffID IN (%Ld)', $load_ids); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $refs = array(); $vs_map = array(); $vs_changesets = array(); if ($diff_vs) { $vs_id = $diff_vs->getID(); $vs_changesets_path_map = array(); foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); $vs_changesets_path_map[$path] = $changeset; $vs_changesets[$changeset->getID()] = $changeset; } foreach ($changesets as $key => $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $target); if (isset($vs_changesets_path_map[$path])) { $vs_map[$changeset->getID()] = $vs_changesets_path_map[$path]->getID(); $refs[$changeset->getID()] = $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); unset($vs_changesets_path_map[$path]); } else { $refs[$changeset->getID()] = $changeset->getID(); } } foreach ($vs_changesets_path_map as $path => $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; $refs[$changeset->getID()] = $changeset->getID().'/-1'; } } else { foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map, $vs_changesets, $refs); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { if (!$aux_field->shouldAppearOnRevisionView()) { unset($aux_fields[$key]); } else { $aux_field->setUser($this->getRequest()->getUser()); } } $aux_fields = DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); return $aux_fields; } private function buildSymbolIndexes( PhabricatorRepositoryArcanistProject $arc_project, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $arc_project->getSymbolIndexLanguages(); if (!$langs) { return array(array(), array()); } $symbol_indexes = array(); $project_phids = array_merge( array($arc_project->getPHID()), nonempty($arc_project->getSymbolIndexProjects(), array())); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'projects' => $project_phids, ); } } return array($symbol_indexes, $project_phids); } private function loadOtherRevisions( array $changesets, DifferentialDiff $target, PhabricatorRepository $repository) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $changeset->getAbsoluteRepositoryPath( $repository, $target); } if (!$paths) { return array(); } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (!$path_map) { return array(); } $query = id(new DifferentialRevisionQuery()) ->setViewer($this->getRequest()->getUser()) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED) ->setLimit(10) ->needRelationships(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); } $results = $query->execute(); // Strip out *this* revision. foreach ($results as $key => $result) { if ($result->getID() == $this->revisionID) { unset($results[$key]); } } return $results; } private function renderOtherRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $user = $this->getRequest()->getUser(); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setFields(DifferentialRevisionListView::getDefaultFields($user)) ->setUser($user) ->loadAssets(); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Open Revisions Affecting These Files')) ->appendChild($view); } /** * Note this code is somewhat similar to the buildPatch method in * @{class:DifferentialReviewRequestMail}. * * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( array $changesets, array $vs_changesets, array $vs_map, PhabricatorRepository $repository = null) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($vs_changesets, 'DifferentialChangeset'); $viewer = $this->getRequest()->getUser(); foreach ($changesets as $changeset) { $changeset->attachHunks($changeset->loadHunks()); } $diff = new DifferentialDiff(); $diff->attachChangesets($changesets); $raw_changes = $diff->buildChangesList(); $changes = array(); foreach ($raw_changes as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $loader = id(new PhabricatorFileBundleLoader()) ->setViewer($viewer); $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setLoadFileDataCallback(array($loader, 'loadFileData')); $vcs = $repository ? $repository->getVersionControlSystem() : null; switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $raw_diff = $bundle->toGitPatch(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: default: $raw_diff = $bundle->toUnifiedDiff(); break; } $request_uri = $this->getRequest()->getRequestURI(); // this ends up being something like // D123.diff // or the verbose // D123.vs123.id123.whitespaceignore-all.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; foreach ($request_uri->getQueryParams() as $key => $value) { if ($key == 'download') { continue; } $file_name .= $key.$value.'.'; } $file_name .= 'diff'; $file = PhabricatorFile::buildFromFileDataOrHash( $raw_diff, array( 'name' => $file_name, )); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } } diff --git a/src/applications/diffusion/controller/DiffusionLintController.php b/src/applications/diffusion/controller/DiffusionLintController.php index ce462e9ad..f826f33aa 100644 --- a/src/applications/diffusion/controller/DiffusionLintController.php +++ b/src/applications/diffusion/controller/DiffusionLintController.php @@ -1,347 +1,345 @@ getRequest(); $user = $this->getRequest()->getUser(); $drequest = $this->diffusionRequest; if ($request->getStr('lint') !== null) { $controller = new DiffusionLintDetailsController($request); $controller->setDiffusionRequest($drequest); $controller->setCurrentApplication($this->getCurrentApplication()); return $this->delegateToController($controller); } $owners = array(); if (!$drequest) { if (!$request->getArr('owner')) { $owners = array($user->getPHID()); } else { $owners = array(head($request->getArr('owner'))); } $owner_handles = $this->loadViewerHandles($owners); } $codes = $this->loadLintCodes($owners); if ($codes && !$drequest) { // TODO: Build some real Query classes for this stuff. $branches = id(new PhabricatorRepositoryBranch())->loadAllWhere( 'id IN (%Ld)', array_unique(ipull($codes, 'branchID'))); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->withIDs(mpull($branches, 'getRepositoryID')) ->execute(); $drequests = array(); foreach ($branches as $id => $branch) { if (empty($repositories[$branch->getRepositoryID()])) { continue; } $drequests[$id] = DiffusionRequest::newFromDictionary(array( 'user' => $user, 'repository' => $repositories[$branch->getRepositoryID()], 'branch' => $branch->getName(), )); } } $rows = array(); $total = 0; foreach ($codes as $code) { if (!$this->diffusionRequest) { $drequest = idx($drequests, $code['branchID']); } if (!$drequest) { continue; } $total += $code['n']; $href_lint = $drequest->generateURI(array( 'action' => 'lint', 'lint' => $code['code'], )); $href_browse = $drequest->generateURI(array( 'action' => 'browse', 'lint' => $code['code'], )); $href_repo = $drequest->generateURI(array('action' => 'lint')); $rows[] = array( phutil_tag('a', array('href' => $href_lint), $code['n']), phutil_tag('a', array('href' => $href_browse), $code['files']), phutil_tag('a', array('href' => $href_repo), $drequest->getCallsign()), ArcanistLintSeverity::getStringForSeverity($code['maxSeverity']), $code['code'], $code['maxName'], $code['maxDescription'], ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array( pht('Problems'), pht('Files'), pht('Repository'), pht('Severity'), pht('Code'), pht('Name'), pht('Example'), )) ->setColumnVisibility(array(true, true, !$this->diffusionRequest)) ->setColumnClasses(array('n', 'n', '', '', 'pri', '', '')); $content = array(); $link = null; if (!$this->diffusionRequest) { $form = id(new AphrontFormView()) ->setUser($user) ->setMethod('GET') ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/users/') ->setLimit(1) ->setName('owner') ->setLabel(pht('Owner')) ->setValue($owner_handles)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Filter')); $content[] = id(new AphrontListFilterView())->appendChild($form); } $content[] = id(new AphrontPanelView()) ->setNoBackground(true) ->setCaption($link) ->appendChild($table); $title = array('Lint'); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'lint', )); if ($this->diffusionRequest) { $title[] = $drequest->getCallsign(); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('All Lint'))); + $crumbs->addTextCrumb(pht('All Lint')); } if ($this->diffusionRequest) { $branch = $drequest->loadBranch(); $header = id(new PHUIHeaderView()) ->setHeader($this->renderPathLinks($drequest, 'lint')) ->setUser($user) ->setPolicyObject($drequest->getRepository()); $actions = $this->buildActionView($drequest); $properties = $this->buildPropertyView( $drequest, $branch, $total, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } else { $object_box = null; } return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => $title, )); } private function loadLintCodes(array $owner_phids) { $drequest = $this->diffusionRequest; $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = array('1 = 1'); if ($drequest) { $branch = $drequest->loadBranch(); if (!$branch) { return array(); } $where[] = qsprintf($conn, 'branchID = %d', $branch->getID()); if ($drequest->getPath() != '') { $path = '/'.$drequest->getPath(); $is_dir = (substr($path, -1) == '/'); $where[] = ($is_dir ? qsprintf($conn, 'path LIKE %>', $path) : qsprintf($conn, 'path = %s', $path)); } } if ($owner_phids) { $or = array(); $or[] = qsprintf($conn, 'authorPHID IN (%Ls)', $owner_phids); $paths = array(); $packages = id(new PhabricatorOwnersOwner()) ->loadAllWhere('userPHID IN (%Ls)', $owner_phids); if ($packages) { $paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID IN (%Ld)', mpull($packages, 'getPackageID')); } if ($paths) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs(mpull($paths, 'getRepositoryPHID')) ->execute(); $repositories = mpull($repositories, 'getID', 'getPHID'); $branches = id(new PhabricatorRepositoryBranch())->loadAllWhere( 'repositoryID IN (%Ld)', $repositories); $branches = mgroup($branches, 'getRepositoryID'); } foreach ($paths as $path) { $branch = idx( $branches, idx( $repositories, $path->getRepositoryPHID())); if ($branch) { $condition = qsprintf( $conn, '(branchID IN (%Ld) AND path LIKE %>)', array_keys($branch), $path->getPath()); if ($path->getExcluded()) { $where[] = 'NOT '.$condition; } else { $or[] = $condition; } } } $where[] = '('.implode(' OR ', $or).')'; } return queryfx_all( $conn, 'SELECT branchID, code, MAX(severity) AS maxSeverity, MAX(name) AS maxName, MAX(description) AS maxDescription, COUNT(DISTINCT path) AS files, COUNT(*) AS n FROM %T WHERE %Q GROUP BY branchID, code ORDER BY n DESC', PhabricatorRepository::TABLE_LINTMESSAGE, implode(' AND ', $where)); } protected function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $list_uri = $drequest->generateURI( array( 'action' => 'lint', 'lint' => '', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View As List')) ->setHref($list_uri) ->setIcon('transcript')); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('history')); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Browse Content')) ->setHref($browse_uri) ->setIcon('file')); return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorRepositoryBranch $branch, $total, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $callsign = $drequest->getRepository()->getCallsign(); $lint_commit = $branch->getLintCommit(); $view->addProperty( pht('Lint Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $lint_commit, )), ), $drequest->getRepository()->formatCommitName($lint_commit))); $view->addProperty( pht('Total Messages'), pht('%s', new PhutilNumber($total))); return $view; } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index f38ddffc2..f6aa65e8f 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -1,770 +1,768 @@ edit = $data['edit']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // NOTE: We can end up here via either "Create Repository", or via // "Import Repository", or via "Edit Remote". In the latter case, we show // only a few of the pages. $repository = null; switch ($this->edit) { case 'remote': $repository = $this->getDiffusionRequest()->getRepository(); // Make sure we have CAN_EDIT. PhabricatorPolicyFilter::requireCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $this->setRepository($repository); $cancel_uri = $this->getRepositoryControllerURI($repository, 'edit/'); break; case 'import': case 'create': $this->requireApplicationCapability( DiffusionCapabilityCreateRepositories::CAPABILITY); $cancel_uri = $this->getApplicationURI('new/'); break; default: throw new Exception("Invalid edit operation!"); } $form = id(new PHUIPagedFormView()) ->setUser($viewer) ->setCancelURI($cancel_uri); switch ($this->edit) { case 'remote': $title = pht('Edit Remote'); $form ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()); break; case 'create': $title = pht('Create Repository'); $form ->addPage('vcs', $this->buildVCSPage()) ->addPage('name', $this->buildNamePage()) ->addPage('done', $this->buildDonePage()); break; case 'import': $title = pht('Import Repository'); $form ->addPage('vcs', $this->buildVCSPage()) ->addPage('name', $this->buildNamePage()) ->addPage('remote-uri', $this->buildRemoteURIPage()) ->addPage('auth', $this->buildAuthPage()) ->addPage('done', $this->buildDonePage()); break; } if ($request->isFormPost()) { $form->readFromRequest($request); if ($form->isComplete()) { $is_create = ($this->edit === 'import' || $this->edit === 'create'); $is_auth = ($this->edit == 'import' || $this->edit == 'remote'); $is_init = ($this->edit == 'create'); if ($is_create) { $repository = PhabricatorRepository::initializeNewRepository( $viewer); } $template = id(new PhabricatorRepositoryTransaction()); $type_name = PhabricatorRepositoryTransaction::TYPE_NAME; $type_vcs = PhabricatorRepositoryTransaction::TYPE_VCS; $type_activate = PhabricatorRepositoryTransaction::TYPE_ACTIVATE; $type_local_path = PhabricatorRepositoryTransaction::TYPE_LOCAL_PATH; $type_remote_uri = PhabricatorRepositoryTransaction::TYPE_REMOTE_URI; $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; $type_credential = PhabricatorRepositoryTransaction::TYPE_CREDENTIAL; $xactions = array(); // If we're creating a new repository, set all this core stuff. if ($is_create) { $callsign = $form->getPage('name') ->getControl('callsign')->getValue(); // We must set this to a unique value to save the repository // initially, and it's immutable, so we don't bother using // transactions to apply this change. $repository->setCallsign($callsign); // Put the repository in "Importing" mode until we finish // parsing it. $repository->setDetail('importing', true); $xactions[] = id(clone $template) ->setTransactionType($type_name) ->setNewValue( $form->getPage('name')->getControl('name')->getValue()); $xactions[] = id(clone $template) ->setTransactionType($type_vcs) ->setNewValue( $form->getPage('vcs')->getControl('vcs')->getValue()); $activate = $form->getPage('done') ->getControl('activate')->getValue(); $xactions[] = id(clone $template) ->setTransactionType($type_activate) ->setNewValue( ($activate == 'start')); $default_local_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); $default_local_path = rtrim($default_local_path, '/'); $default_local_path = $default_local_path.'/'.$callsign.'/'; $xactions[] = id(clone $template) ->setTransactionType($type_local_path) ->setNewValue($default_local_path); } if ($is_init) { $xactions[] = id(clone $template) ->setTransactionType($type_hosting) ->setNewValue(true); } if ($is_auth) { $xactions[] = id(clone $template) ->setTransactionType($type_remote_uri) ->setNewValue( $form->getPage('remote-uri')->getControl('remoteURI') ->getValue()); $xactions[] = id(clone $template) ->setTransactionType($type_credential) ->setNewValue( $form->getPage('auth')->getControl('credential')->getValue()); } id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); $repo_uri = $this->getRepositoryControllerURI($repository, 'edit/'); return id(new AphrontRedirectResponse())->setURI($repo_uri); } } else { $dict = array(); if ($repository) { $dict = array( 'remoteURI' => $repository->getRemoteURI(), 'credential' => $repository->getCredentialPHID(), ); } $form->readFromObject($dict); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form, ), array( 'title' => $title, 'device' => true, )); } /* -( Page: VCS Type )----------------------------------------------------- */ private function buildVCSPage() { $is_import = ($this->edit == 'import'); if ($is_import) { $git_str = pht( 'Import a Git repository (for example, a repository hosted '. 'on GitHub).'); $hg_str = pht( 'Import a Mercurial repository (for example, a repository '. 'hosted on Bitbucket).'); $svn_str = pht('Import a Subversion repository.'); } else { $git_str = pht('Create a new, empty Git repository.'); $hg_str = pht('Create a new, empty Mercurial repository.'); $svn_str = pht('Create a new, empty Subversion repository.'); } $control = id(new AphrontFormRadioButtonControl()) ->setName('vcs') ->setLabel(pht('Type')) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT, pht('Git'), $git_str) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL, pht('Mercurial'), $hg_str) ->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_SVN, pht('Subversion'), $svn_str); if ($is_import) { $control->addButton( PhabricatorRepositoryType::REPOSITORY_TYPE_PERFORCE, pht('Perforce'), pht( 'Perforce is not directly supported, but you can import '. 'a Perforce repository as a Git repository using %s.', phutil_tag( 'a', array( 'href' => 'http://www.perforce.com/product/components/git-fusion', 'target' => '_blank', ), pht('Perforce Git Fusion'))), 'disabled', $disabled = true); } return id(new PHUIFormPageView()) ->setPageName(pht('Repository Type')) ->setUser($this->getRequest()->getUser()) ->setValidateFormPageCallback(array($this, 'validateVCSPage')) ->addControl($control); } public function validateVCSPage(PHUIFormPageView $page) { $valid = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => true, PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => true, PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => true, ); $c_vcs = $page->getControl('vcs'); $v_vcs = $c_vcs->getValue(); if (!$v_vcs) { $c_vcs->setError(pht('Required')); $page->addPageError( pht('You must select a version control system.')); } else if (empty($valid[$v_vcs])) { $c_vcs->setError(pht('Invalid')); $page->addPageError( pht('You must select a valid version control system.')); } return $c_vcs->isValid(); } /* -( Page: Name and Callsign )-------------------------------------------- */ private function buildNamePage() { return id(new PHUIFormPageView()) ->setUser($this->getRequest()->getUser()) ->setPageName(pht('Repository Name and Location')) ->setValidateFormPageCallback(array($this, 'validateNamePage')) ->addRemarkupInstructions( pht( '**Choose a human-readable name for this repository**, like '. '"CompanyName Mobile App" or "CompanyName Backend Server". You '. 'can change this later.')) ->addControl( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setCaption(pht('Human-readable repository name.'))) ->addRemarkupInstructions( pht( '**Choose a "Callsign" for the repository.** This is a short, '. 'unique string which identifies commits elsewhere in Phabricator. '. 'For example, you might use `M` for your mobile app repository '. 'and `B` for your backend repository.'. "\n\n". '**Callsigns must be UPPERCASE**, and can not be edited after the '. 'repository is created. Generally, you should choose short '. 'callsigns.')) ->addControl( id(new AphrontFormTextControl()) ->setName('callsign') ->setLabel(pht('Callsign')) ->setCaption(pht('Short UPPERCASE identifier.'))); } public function validateNamePage(PHUIFormPageView $page) { $c_name = $page->getControl('name'); $v_name = $c_name->getValue(); if (!strlen($v_name)) { $c_name->setError(pht('Required')); $page->addPageError( pht('You must choose a name for this repository.')); } $c_call = $page->getControl('callsign'); $v_call = $c_call->getValue(); if (!strlen($v_call)) { $c_call->setError(pht('Required')); $page->addPageError( pht('You must choose a callsign for this repository.')); } else if (!preg_match('/^[A-Z]+$/', $v_call)) { $c_call->setError(pht('Invalid')); $page->addPageError( pht('The callsign must contain only UPPERCASE letters.')); } else { $exists = false; try { $repo = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getRequest()->getUser()) ->withCallsigns(array($v_call)) ->executeOne(); $exists = (bool)$repo; } catch (PhabricatorPolicyException $ex) { $exists = true; } if ($exists) { $c_call->setError(pht('Not Unique')); $page->addPageError( pht( 'Another repository already uses that callsign. You must choose '. 'a unique callsign.')); } } return $c_name->isValid() && $c_call->isValid(); } /* -( Page: Remote URI )--------------------------------------------------- */ private function buildRemoteURIPage() { return id(new PHUIFormPageView()) ->setUser($this->getRequest()->getUser()) ->setPageName(pht('Repository Remote URI')) ->setValidateFormPageCallback(array($this, 'validateRemoteURIPage')) ->setAdjustFormPageCallback(array($this, 'adjustRemoteURIPage')) ->addControl( id(new AphrontFormTextControl()) ->setName('remoteURI')); } public function adjustRemoteURIPage(PHUIFormPageView $page) { $form = $page->getForm(); $is_git = false; $is_svn = false; $is_mercurial = false; if ($this->getRepository()) { $vcs = $this->getRepository()->getVersionControlSystem(); } else { $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); } switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $is_svn = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_mercurial = true; break; default: throw new Exception("Unsupported VCS!"); } $has_local = ($is_git || $is_mercurial); if ($is_git) { $uri_label = pht('Remote URI'); $instructions = pht( 'Enter the URI to clone this Git repository from. It should usually '. 'look like one of these examples:'. "\n\n". "| Example Git Remote URIs |\n". "| ----------------------- |\n". "| `git@github.com:example/example.git` |\n". "| `ssh://user@host.com/git/example.git` |\n". "| `file:///local/path/to/repo` |\n". "| `https://example.com/repository.git` |\n"); } else if ($is_mercurial) { $uri_label = pht('Remote URI'); $instructions = pht( 'Enter the URI to clone this Mercurial repository from. It should '. 'usually look like one of these examples:'. "\n\n". "| Example Mercurial Remote URIs |\n". "| ----------------------- |\n". "| `ssh://hg@bitbucket.org/example/repository` |\n". "| `file:///local/path/to/repo` |\n"); } else if ($is_svn) { $uri_label = pht('Repository Root'); $instructions = pht( 'Enter the **Repository Root** for this Subversion repository. '. 'You can figure this out by running `svn info` in a working copy '. 'and looking at the value in the `Repository Root` field. It '. 'should be a URI and will usually look like these:'. "\n\n". "| Example Subversion Repository Root URIs |\n". "| ------------------------------ |\n". "| `http://svn.example.org/svnroot/` |\n". "| `svn+ssh://svn.example.com/svnroot/` |\n". "| `svn://svn.example.net/svnroot/` |\n". "| `file:///local/path/to/svnroot/` |\n". "\n\n". "Make sure you specify the root of the repository, not a ". "subdirectory."); } else { throw new Exception("Unsupported VCS!"); } $page->addRemarkupInstructions($instructions, 'remoteURI'); $page->getControl('remoteURI')->setLabel($uri_label); } public function validateRemoteURIPage(PHUIFormPageView $page) { $c_remote = $page->getControl('remoteURI'); $v_remote = $c_remote->getValue(); if (!strlen($v_remote)) { $c_remote->setError(pht('Required')); $page->addPageError( pht("You must specify a URI.")); } else { $proto = $this->getRemoteURIProtocol($v_remote); if ($proto === 'file') { if (!preg_match('@^file:///@', $v_remote)) { $c_remote->setError(pht('Invalid')); $page->addPageError( pht( "URIs using the 'file://' protocol should have three slashes ". "(e.g., 'file:///absolute/path/to/file'). You only have two. ". "Add another one.")); } } // Catch confusion between Git/SCP-style URIs and normal URIs. See T3619 // for discussion. This is usually a user adding "ssh://" to an implicit // SSH Git URI. if ($proto == 'ssh') { if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $v_remote)) { $c_remote->setError(pht('Invalid')); $page->addPageError( pht( "The Remote URI is not formatted correctly. Remote URIs ". "with an explicit protocol should be in the form ". "'proto://domain/path', not 'proto://domain:/path'. ". "The ':/path' syntax is only valid in SCP-style URIs.")); } } switch ($proto) { case 'ssh': case 'http': case 'https': case 'file': case 'git': case 'svn': case 'svn+ssh': break; default: $c_remote->setError(pht('Invalid')); $page->addPageError( pht( "The URI protocol is unrecognized. It should begin ". "'ssh://', 'http://', 'https://', 'file://', 'git://', ". "'svn://', 'svn+ssh://', or be in the form ". "'git@domain.com:path'.")); break; } } return $c_remote->isValid(); } /* -( Page: Authentication )----------------------------------------------- */ public function buildAuthPage() { return id(new PHUIFormPageView()) ->setPageName(pht('Authentication')) ->setUser($this->getRequest()->getUser()) ->setAdjustFormPageCallback(array($this, 'adjustAuthPage')) ->addControl( id(new PassphraseCredentialControl()) ->setName('credential')); } public function adjustAuthPage($page) { $form = $page->getForm(); if ($this->getRepository()) { $vcs = $this->getRepository()->getVersionControlSystem(); } else { $vcs = $form->getPage('vcs')->getControl('vcs')->getValue(); } $remote_uri = $form->getPage('remote-uri') ->getControl('remoteURI') ->getValue(); $proto = $this->getRemoteURIProtocol($remote_uri); $remote_user = $this->getRemoteURIUser($remote_uri); $c_credential = $page->getControl('credential'); $c_credential->setDefaultUsername($remote_user); if ($this->isSSHProtocol($proto)) { $c_credential->setLabel(pht('SSH Key')); $c_credential->setCredentialType( PassphraseCredentialTypeSSHPrivateKeyText::CREDENTIAL_TYPE); $provides_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( 'Choose or add the SSH credentials to use to connect to the the '. 'repository hosted at:'. "\n\n". " lang=text\n". " %s", $remote_uri), 'credential'); } else if ($this->isUsernamePasswordProtocol($proto)) { $c_credential->setLabel(pht('Password')); $c_credential->setAllowNull(true); $c_credential->setCredentialType( PassphraseCredentialTypePassword::CREDENTIAL_TYPE); $provides_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; $page->addRemarkupInstructions( pht( 'Choose the a username and pasword used to connect to the '. 'repository hosted at:'. "\n\n". " lang=text\n". " %s". "\n\n". "If this repository does not require a username or password, ". "you can continue to the next step.", $remote_uri), 'credential'); } else if ($proto == 'file') { $c_credential->setHidden(true); $provides_type = null; $page->addRemarkupInstructions( pht( 'You do not need to configure any credentials for repositories '. 'accessed over the `file://` protocol. Continue to the next step.'), 'credential'); } else { throw new Exception("Unknown URI protocol!"); } if ($provides_type) { $viewer = $this->getRequest()->getUser(); $options = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIsDestroyed(false) ->withProvidesTypes(array($provides_type)) ->execute(); $c_credential->setOptions($options); } } public function validateAuthPage(PHUIFormPageView $page) { $form = $page->getForm(); $remote_uri = $form->getPage('remote')->getControl('remoteURI')->getValue(); $proto = $this->getRemoteURIProtocol($remote_uri); $c_credential = $page->getControl('credential'); $v_credential = $c_credential->getValue(); // NOTE: We're using the omnipotent user here because the viewer might be // editing a repository they're allowed to edit which uses a credential they // are not allowed to see. This is fine, as long as they don't change it. $credential = id(new PassphraseCredentialQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($v_credential)) ->executeOne(); if ($this->isSSHProtocol($proto)) { if (!$credential) { $c_credential->setError(pht('Required')); $page->addPageError( pht('You must choose an SSH credential to connect over SSH.')); } $ssh_type = PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE; if ($credential->getProvidesType() !== $ssh_type) { $c_credential->setError(pht('Invalid')); $page->addPageError( pht( 'You must choose an SSH credential, not some other type '. 'of credential.')); } } else if ($this->isUsernamePasswordProtocol($proto)) { if ($credential) { $password_type = PassphraseCredentialTypePassword::PROVIDES_TYPE; if ($credential->getProvidesType() !== $password_type) { $c_credential->setError(pht('Invalid')); $page->addPageError( pht( 'You must choose a username/password credential, not some other '. 'type of credential.')); } } return $c_credential->isValid(); } else { return true; } } /* -( Page: Done )--------------------------------------------------------- */ private function buildDonePage() { $is_create = ($this->edit == 'create'); if ($is_create) { $now_label = pht('Create Repository Now'); $now_caption = pht( 'Create the repository right away. This will create the repository '. 'using default settings.'); $wait_label = pht('Configure More Options First'); $wait_caption = pht( 'Configure more options before creating the repository. '. 'This will let you fine-tune settings. You can create the repository '. 'whenever you are ready.'); } else { $now_label = pht('Start Import Now'); $now_caption = pht( 'Start importing the repository right away. This will import '. 'the entire repository using default settings.'); $wait_label = pht('Configure More Options First'); $wait_caption = pht( 'Configure more options before beginning the repository '. 'import. This will let you fine-tune settings. You can '. 'start the import whenever you are ready.'); } return id(new PHUIFormPageView()) ->setPageName(pht('Repository Ready!')) ->setValidateFormPageCallback(array($this, 'validateDonePage')) ->setUser($this->getRequest()->getUser()) ->addControl( id(new AphrontFormRadioButtonControl()) ->setName('activate') ->setLabel(pht('Start Now')) ->addButton( 'start', $now_label, $now_caption) ->addButton( 'wait', $wait_label, $wait_caption)); } public function validateDonePage(PHUIFormPageView $page) { $c_activate = $page->getControl('activate'); $v_activate = $c_activate->getValue(); if ($v_activate != 'start' && $v_activate != 'wait') { $c_activate->setError(pht('Required')); $page->addPageError( pht('Make a choice about repository activation.')); } return $c_activate->isValid(); } /* -( Internal )----------------------------------------------------------- */ private function getRemoteURIProtocol($raw_uri) { $uri = new PhutilURI($raw_uri); if ($uri->getProtocol()) { return strtolower($uri->getProtocol()); } $git_uri = new PhutilGitURI($raw_uri); if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { return 'ssh'; } return null; } private function getRemoteURIUser($raw_uri) { $uri = new PhutilURI($raw_uri); if ($uri->getUser()) { return $uri->getUser(); } $git_uri = new PhutilGitURI($raw_uri); if (strlen($git_uri->getDomain()) && strlen($git_uri->getPath())) { return $git_uri->getUser(); } return null; } private function isSSHProtocol($proto) { return ($proto == 'git' || $proto == 'ssh' || $proto == 'svn+ssh'); } private function isUsernamePasswordProtocol($proto) { return ($proto == 'http' || $proto == 'https' || $proto == 'svn'); } private function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } private function getRepository() { return $this->repository; } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php index 67e3b7aa7..fe2e2f818 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActionsController.php @@ -1,126 +1,124 @@ getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); // NOTE: We're inverting these here, because the storage is silly. $v_notify = !$repository->getHumanReadableDetail('herald-disabled'); $v_autoclose = !$repository->getHumanReadableDetail('disable-autoclose'); if ($request->isFormPost()) { $v_notify = $request->getBool('notify'); $v_autoclose = $request->getBool('autoclose'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_notify = PhabricatorRepositoryTransaction::TYPE_NOTIFY; $type_autoclose = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE; $xactions[] = id(clone $template) ->setTransactionType($type_notify) ->setNewValue($v_notify); $xactions[] = id(clone $template) ->setTransactionType($type_autoclose) ->setNewValue($v_autoclose); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Actions'))); + $crumbs->addTextCrumb(pht('Edit Actions')); $title = pht('Edit Actions (%s)', $repository->getName()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( "Normally, Phabricator publishes notifications when it discovers ". "new commits. You can disable publishing for this repository by ". "turning off **Notify/Publish**. This will disable notifications, ". "feed, and Herald for this repository.". "\n\n". "When Phabricator discovers a new commit, it can automatically ". "close associated revisions and tasks. If you don't want ". "Phabricator to close objects when it discovers new commits in ". "this repository, you can disable **Autoclose**.")) ->appendChild( id(new AphrontFormSelectControl()) ->setName('notify') ->setLabel(pht('Notify/Publish')) ->setValue((int)$v_notify) ->setOptions( array( 1 => pht('Enable Notifications, Feed and Herald'), 0 => pht('Disable Notifications, Feed and Herald'), ))) ->appendChild( id(new AphrontFormSelectControl()) ->setName('autoclose') ->setLabel(pht('Autoclose')) ->setValue((int)$v_autoclose) ->setOptions( array( 1 => pht('Enable Autoclose'), 0 => pht('Disable Autoclose'), ))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Actions')) ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php index 62b58da01..7729b5d16 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php @@ -1,135 +1,133 @@ getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_name = $repository->getName(); $v_desc = $repository->getDetail('description'); $e_name = true; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); if (!strlen($v_name)) { $e_name = pht('Required'); $errors[] = pht('Repository name is required.'); } else { $e_name = null; } if (!$errors) { $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_name = PhabricatorRepositoryTransaction::TYPE_NAME; $type_desc = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION; $xactions[] = id(clone $template) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(clone $template) ->setTransactionType($type_desc) ->setNewValue($v_desc); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Basics'))); + $crumbs->addTextCrumb(pht('Edit Basics')); $title = pht('Edit %s', $repository->getName()); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($edit_uri)) ->appendChild(id(new PHUIFormDividerControl())) ->appendRemarkupInstructions($this->getReadmeInstructions()); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormError($error_view); return $this->buildApplicationPage( array( $crumbs, $object_box), array( 'title' => $title, 'device' => true, )); } private function getReadmeInstructions() { return pht(<<getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $is_git = false; $is_hg = false; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_hg = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: throw new Exception( pht('Subversion does not support branches!')); default: throw new Exception( pht('Repository has unknown version control system!')); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_default = $repository->getHumanReadableDetail('default-branch'); $v_track = $repository->getHumanReadableDetail( 'branch-filter', array()); $v_autoclose = $repository->getHumanReadableDetail( 'close-commits-filter', array()); if ($request->isFormPost()) { $v_default = $request->getStr('default'); $v_track = $request->getStrList('track'); $v_autoclose = $request->getStrList('autoclose'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_default = PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH; $type_track = PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY; $type_autoclose = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY; $xactions[] = id(clone $template) ->setTransactionType($type_default) ->setNewValue($v_default); $xactions[] = id(clone $template) ->setTransactionType($type_track) ->setNewValue($v_track); if (!$is_hg) { $xactions[] = id(clone $template) ->setTransactionType($type_autoclose) ->setNewValue($v_autoclose); } id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Branches'))); + $crumbs->addTextCrumb(pht('Edit Branches')); $title = pht('Edit Branches (%s)', $repository->getName()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( 'You can choose a **Default Branch** for viewing this repository.'. "\n\n". 'If you want to import only some branches into Diffusion, you can '. 'list them in **Track Only**. Other branches will be ignored. If '. 'you do not specify any branches, all branches are tracked.'. "\n\n". 'If you have **Autoclose** enabled, Phabricator can close tasks and '. 'revisions when corresponding commits are pushed to the repository. '. 'If you want to autoclose objects only when commits appear on '. 'specific branches, you can list those branches in **Autoclose '. 'Only**. By default, all branches autoclose objects.')) ->appendChild( id(new AphrontFormTextControl()) ->setName('default') ->setLabel(pht('Default Branch')) ->setValue($v_default) ->setCaption( pht('Example: %s', phutil_tag('tt', array(), 'develop')))) ->appendChild( id(new AphrontFormTextControl()) ->setName('track') ->setLabel(pht('Track Only')) ->setValue($v_track) ->setCaption( pht('Example: %s', phutil_tag('tt', array(), 'master, develop')))); if (!$is_hg) { $form->appendChild( id(new AphrontFormTextControl()) ->setName('autoclose') ->setLabel(pht('Autoclose Only')) ->setValue($v_autoclose) ->setCaption( pht('Example: %s', phutil_tag('tt', array(), 'master, release')))); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Branches')) ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditController.php index 292597cbf..6b8deca4f 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditController.php @@ -1,34 +1,26 @@ diffusionRequest) { $repository = $this->getDiffusionRequest()->getRepository(); $repo_uri = $this->getRepositoryControllerURI($repository, ''); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('r'.$repository->getCallsign()) - ->setHref($repo_uri)); + $crumbs->addTextCrumb('r'.$repository->getCallsign(), $repo_uri); if ($is_main) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Repository'))); + $crumbs->addTextCrumb(pht('Edit Repository')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit')) - ->setHref($edit_uri)); + $crumbs->addTextCrumb(pht('Edit'), $edit_uri); } } return $crumbs; } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php index 24ad783e0..eebc6d193 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditEncodingController.php @@ -1,122 +1,120 @@ getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_encoding = $repository->getDetail('encoding'); $e_encoding = null; $errors = array(); if ($request->isFormPost()) { $v_encoding = $request->getStr('encoding'); if (!$errors) { $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_encoding = PhabricatorRepositoryTransaction::TYPE_ENCODING; $xactions[] = id(clone $template) ->setTransactionType($type_encoding) ->setNewValue($v_encoding); try { id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } catch (Exception $ex) { $errors[] = $ex->getMessage(); } } } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Encoding'))); + $crumbs->addTextCrumb(pht('Edit Encoding')); $title = pht('Edit %s', $repository->getName()); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions($this->getEncodingInstructions()) ->appendChild( id(new AphrontFormTextControl()) ->setName('encoding') ->setLabel(pht('Text Encoding')) ->setValue($v_encoding) ->setError($e_encoding)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Encoding')) ->addCancelButton($edit_uri)); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormError($error_view); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } private function getEncodingInstructions() { return pht(<<serve = idx($data, 'serve'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } if (!$this->serve) { return $this->handleHosting($repository); } else { return $this->handleProtocols($repository); } } public function handleHosting(PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $v_hosting = $repository->isHosted(); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $next_uri = $this->getRepositoryControllerURI($repository, 'edit/serve/'); if ($request->isFormPost()) { $v_hosting = $request->getBool('hosting'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; $xactions[] = id(clone $template) ->setTransactionType($type_hosting) ->setNewValue($v_hosting); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($next_uri); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Hosting'))); + $crumbs->addTextCrumb(pht('Edit Hosting')); $title = pht('Edit Hosting (%s)', $repository->getName()); $hosted_control = id(new AphrontFormRadioButtonControl()) ->setName('hosting') ->setLabel(pht('Hosting')) ->addButton( true, pht('Host Repository on Phabricator'), pht( 'Phabricator will host this repository. Users will be able to '. 'push commits to Phabricator. Phabricator will not pull '. 'changes from elsewhere.')) ->addButton( false, pht('Host Repository Elsewhere'), pht( 'Phabricator will pull updates to this repository from a master '. 'repository elsewhere (for example, on GitHub or Bitbucket). '. 'Users will not be able to push commits to this repository.')) ->setValue($v_hosting); $doc_href = PhabricatorEnv::getDoclink( 'article/Diffusion_User_Guide_Repository_Hosting.html'); $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( 'Phabricator can host repositories, or it can track repositories '. 'hosted elsewhere (like on GitHub or Bitbucket). For information '. 'on configuring hosting, see [[ %s | Diffusion User Guide: '. 'Repository Hosting]]', $doc_href)) ->appendChild($hosted_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save and Continue')) ->addCancelButton($edit_uri)); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } public function handleProtocols(PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $type = $repository->getVersionControlSystem(); $is_svn = ($type == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); $v_http_mode = $repository->getDetail( 'serve-over-http', PhabricatorRepository::SERVE_OFF); $v_ssh_mode = $repository->getDetail( 'serve-over-ssh', PhabricatorRepository::SERVE_OFF); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $prev_uri = $this->getRepositoryControllerURI($repository, 'edit/hosting/'); if ($request->isFormPost()) { $v_http_mode = $request->getStr('http'); $v_ssh_mode = $request->getStr('ssh'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP; $type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH; if (!$is_svn) { $xactions[] = id(clone $template) ->setTransactionType($type_http) ->setNewValue($v_http_mode); } $xactions[] = id(clone $template) ->setTransactionType($type_ssh) ->setNewValue($v_ssh_mode); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Protocols'))); + $crumbs->addTextCrumb(pht('Edit Protocols')); $title = pht('Edit Protocols (%s)', $repository->getName()); $rw_message = pht( 'Phabricator will serve a read-write copy of this repository.'); if (!$repository->isHosted()) { $rw_message = array( $rw_message, phutil_tag('br'), phutil_tag('br'), pht( '%s: This repository is hosted elsewhere, so Phabricator can not '. 'perform writes. This mode will act like "Read Only" for '. 'repositories hosted elsewhere.', phutil_tag('strong', array(), 'WARNING'))); } $ssh_control = id(new AphrontFormRadioButtonControl()) ->setName('ssh') ->setLabel(pht('SSH')) ->setValue($v_ssh_mode) ->addButton( PhabricatorRepository::SERVE_OFF, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_OFF), pht('Phabricator will not serve this repository over SSH.')) ->addButton( PhabricatorRepository::SERVE_READONLY, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READONLY), pht( 'Phabricator will serve a read-only copy of this repository '. 'over SSH.')) ->addButton( PhabricatorRepository::SERVE_READWRITE, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READWRITE), $rw_message); $http_control = id(new AphrontFormRadioButtonControl()) ->setName('http') ->setLabel(pht('HTTP')) ->setValue($v_http_mode) ->addButton( PhabricatorRepository::SERVE_OFF, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_OFF), pht('Phabricator will not serve this repository over HTTP.')) ->addButton( PhabricatorRepository::SERVE_READONLY, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READONLY), pht( 'Phabricator will serve a read-only copy of this repository '. 'over HTTP.')) ->addButton( PhabricatorRepository::SERVE_READWRITE, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READWRITE), $rw_message); if ($is_svn) { $http_control = id(new AphrontFormMarkupControl()) ->setLabel(pht('HTTP')) ->setValue( phutil_tag( 'em', array(), pht( 'Phabricator does not currently support HTTP access to '. 'Subversion repositories.'))); } $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( 'Phabricator can serve repositories over various protocols. You can '. 'configure server protocols here.')) ->appendChild($ssh_control); if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { $form->appendRemarkupInstructions( pht( 'NOTE: The configuration setting [[ %s | %s ]] is currently '. 'disabled. You must enable it to activate authenticated access '. 'to repositories over HTTP.', '/config/edit/diffusion.allow-http-auth/', 'diffusion.allow-http-auth')); } $form ->appendChild($http_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Changes')) ->addCancelButton($prev_uri, pht('Back'))); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php index 225ca9bd3..34e66e21b 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditLocalController.php @@ -1,79 +1,77 @@ getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_local = $repository->getHumanReadableDetail('local-path'); $errors = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Local'))); + $crumbs->addTextCrumb(pht('Edit Local')); $title = pht('Edit %s', $repository->getName()); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( "You can not adjust the local path for this repository from the ". "web interface. To edit it, run this command:\n\n". " phabricator/ $ ./bin/repository edit %s --as %s --local-path ...", $repository->getCallsign(), $user->getUsername())) ->appendChild( id(new AphrontFormMarkupControl()) ->setName('local') ->setLabel(pht('Local Path')) ->setValue($v_local)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($edit_uri, pht('Done'))); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormError($error_view); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php index 1a6f7dff2..47ca660c2 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditPolicyController.php @@ -1,135 +1,133 @@ getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_view = $repository->getViewPolicy(); $v_edit = $repository->getEditPolicy(); $v_push = $repository->getPushPolicy(); if ($request->isFormPost()) { $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_push = $request->getStr('pushPolicy'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_push = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY; $xactions[] = id(clone $template) ->setTransactionType($type_view) ->setNewValue($v_view); $xactions[] = id(clone $template) ->setTransactionType($type_edit) ->setNewValue($v_edit); if ($repository->isHosted()) { $xactions[] = id(clone $template) ->setTransactionType($type_push) ->setNewValue($v_push); } id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Policies'))); + $crumbs->addTextCrumb(pht('Edit Policies')); $title = pht('Edit Policies (%s)', $repository->getName()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('editPolicy')); if ($repository->isHosted()) { $form->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(DiffusionCapabilityPush::CAPABILITY) ->setPolicyObject($repository) ->setPolicies($policies) ->setName('pushPolicy')); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Can Push')) ->setValue( phutil_tag('em', array(), pht('Not a Hosted Repository')))); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Policies')) ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php index 425d28d38..a4985e7f3 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php @@ -1,125 +1,123 @@ getRequest(); $viewer = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: throw new Exception( pht('Git and Mercurial do not support editing SVN properties!')); case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; default: throw new Exception( pht('Repository has unknown version control system!')); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_subpath = $repository->getHumanReadableDetail('svn-subpath'); $v_uuid = $repository->getUUID(); if ($request->isFormPost()) { $v_subpath = $request->getStr('subpath'); $v_uuid = $request->getStr('uuid'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_subpath = PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH; $type_uuid = PhabricatorRepositoryTransaction::TYPE_UUID; $xactions[] = id(clone $template) ->setTransactionType($type_subpath) ->setNewValue($v_subpath); $xactions[] = id(clone $template) ->setTransactionType($type_uuid) ->setNewValue($v_uuid); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($viewer) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $content = array(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Subversion Info'))); + $crumbs->addTextCrumb(pht('Edit Subversion Info')); $title = pht('Edit Subversion Info (%s)', $repository->getName()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($repository) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( "You can set the **Repository UUID**, which will help Phabriactor ". "provide better context in some cases. You can find the UUID of a ". "repository by running `svn info`.". "\n\n". "If you want to import only part of a repository, like `trunk/`, ". "you can set a path in **Import Only**. Phabricator will ignore ". "commits which do not affect this path.")) ->appendChild( id(new AphrontFormTextControl()) ->setName('uuid') ->setLabel(pht('Repository UUID')) ->setValue($v_uuid)) ->appendChild( id(new AphrontFormTextControl()) ->setName('subpath') ->setLabel(pht('Import Only')) ->setValue($v_subpath)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Subversion Info')) ->addCancelButton($edit_uri)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php index 5700b8a57..ecc4b5d1e 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryNewController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryNewController.php @@ -1,89 +1,87 @@ getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( DiffusionCapabilityCreateRepositories::CAPABILITY); if ($request->isFormPost()) { if ($request->getStr('type')) { switch ($request->getStr('type')) { case 'create': $uri = $this->getApplicationURI('create/'); break; case 'import': default: $uri = $this->getApplicationURI('import/'); break; } return id(new AphrontRedirectResponse())->setURI($uri); } } $doc_href = PhabricatorEnv::getDoclink( 'article/Diffusion_User_Guide_Repository_Hosting.html'); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Diffusion User Guide: Repository Hosting')); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormRadioButtonControl()) ->setName('type') ->addButton( 'create', pht('Create a New Hosted Repository'), array( pht( 'Create a new, empty repository which Phabricator will host. '. 'For instructions on configuring repository hosting, see %s. '. 'This feature is new and in beta!', $doc_link), )) ->addButton( 'import', pht('Import an Existing External Repository'), pht( 'Import a repository hosted somewhere else, like GitHub, '. 'Bitbucket, or your organization\'s existing servers. '. 'Phabricator will read changes from the repository but will '. 'not host or manage it. The authoritative master version of '. 'the repository will stay where it is now.'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($this->getApplicationURI())); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Repository'))); + $crumbs->addTextCrumb(pht('New Repository')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create or Import Repository')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('New Repository'), 'device' => true, )); } } diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d3895fc4a..d968e85ee 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,676 +1,673 @@ bookName = $data['book']; $this->atomType = $data['type']; $this->atomName = $data['name']; $this->atomContext = nonempty(idx($data, 'context'), null); $this->atomIndex = nonempty(idx($data, 'index'), null); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); require_celerity_resource('diviner-shared-css'); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. // Instead, we might want to load ghosts and render a message like // "this thing existed in an older version, but no longer does", especially // if we add content like comments. $symbol = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->withTypes(array($this->atomType)) ->withNames(array($this->atomName)) ->withContexts(array($this->atomContext)) ->withIndexes(array($this->atomIndex)) ->needAtoms(true) ->needExtends(true) ->needChildren(true) ->executeOne(); if (!$symbol) { return new Aphront404Response(); } $atom = $symbol->getAtom(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($book->getShortTitle()) - ->setHref('/book/'.$book->getName().'/')); + $crumbs->addTextCrumb( + $book->getShortTitle(), + '/book/'.$book->getName().'/'); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($atom_short_title)); + $crumbs->addTextCrumb($atom_short_title); $header = id(new PHUIHeaderView()) ->setHeader($this->renderFullSignature($symbol)) ->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); $properties = id(new PHUIPropertyListView()); $group = $atom->getProperty('group'); if ($group) { $group_name = $book->getGroupName($group); } else { $group_name = null; } $this->buildDefined($properties, $symbol); $this->buildExtendsAndImplements($properties, $symbol); $warnings = $atom->getWarnings(); if ($warnings) { $warnings = id(new AphrontErrorView()) ->setErrors($warnings) ->setTitle(pht('Documentation Warnings')) ->setSeverity(AphrontErrorView::SEVERITY_WARNING); } $methods = $this->composeMethods($symbol); $field = 'default'; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($symbol, $field); foreach ($methods as $method) { foreach ($method['atoms'] as $matom) { $engine->addObject($matom, $field); } } $engine->process(); $content = $this->renderDocumentationText($symbol, $engine); $toc = $engine->getEngineMetadata( $symbol, $field, PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC, array()); $document = id(new PHUIDocumentView()) ->setBook($book->getTitle(), $group_name) ->setHeader($header) ->appendChild($properties) ->appendChild($warnings) ->appendChild($content); $document->appendChild($this->buildParametersAndReturn(array($symbol))); if ($methods) { $tasks = $this->composeTasks($symbol); if ($tasks) { $methods_by_task = igroup($methods, 'task'); // Add phantom tasks for methods which have a "@task" name that isn't // documented anywhere, or methods that have no "@task" name. foreach ($methods_by_task as $task => $ignored) { if (empty($tasks[$task])) { $tasks[$task] = array( 'name' => $task, 'title' => $task ? $task : pht('Other Methods'), 'defined' => $symbol, ); } } $section = id(new DivinerSectionView()) ->setHeader(pht('Tasks')); foreach ($tasks as $spec) { $section->addContent( id(new PHUIHeaderView()) ->setNoBackground(true) ->setHeader($spec['title'])); $task_methods = idx($methods_by_task, $spec['name'], array()); $inner_box = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE_LEFT) ->addPadding(PHUI::PADDING_LARGE_RIGHT) ->addPadding(PHUI::PADDING_LARGE_BOTTOM); $box_content = array(); if ($task_methods) { $list_items = array(); foreach ($task_methods as $task_method) { $atom = last($task_method['atoms']); $item = $this->renderFullSignature($atom, true); if (strlen($atom->getSummary())) { $item = array( $item, " \xE2\x80\x94 ", $atom->getSummary()); } $list_items[] = phutil_tag('li', array(), $item); } $box_content[] = phutil_tag( 'ul', array( 'class' => 'diviner-list', ), $list_items); } else { $no_methods = pht('No methods for this task.'); $box_content = phutil_tag('em', array(), $no_methods); } $inner_box->appendChild($box_content); $section->addContent($inner_box); } $document->appendChild($section); } $section = id(new DivinerSectionView()) ->setHeader(pht('Methods')); foreach ($methods as $spec) { $matom = last($spec['atoms']); $method_header = id(new PHUIHeaderView()) ->setNoBackground(true); $inherited = $spec['inherited']; if ($inherited) { $method_header->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setBackgroundColor(PhabricatorTagView::COLOR_GREY) ->setName(pht('Inherited'))); } $method_header->setHeader($this->renderFullSignature($matom)); $section->addContent( array( $method_header, $this->renderMethodDocumentationText($symbol, $spec, $engine), $this->buildParametersAndReturn($spec['atoms']), )); } $document->appendChild($section); } if ($toc) { $side = new PHUIListView(); $side->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Contents')) ->setType(PHUIListItemView::TYPE_LABEL)); foreach ($toc as $key => $entry) { $side->addMenuItem( id(new PHUIListItemView()) ->setName($entry[1]) ->setHref('#'.$key)); } $document->setSideNav($side, PHUIDocumentView::NAV_TOP); } return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $symbol->getTitle(), 'device' => true, )); } private function buildExtendsAndImplements( PHUIPropertyListView $view, DivinerLiveSymbol $symbol) { $lineage = $this->getExtendsLineage($symbol); if ($lineage) { $tags = array(); foreach ($lineage as $item) { $tags[] = $this->renderAtomTag($item); } $caret = phutil_tag('span', array('class' => 'caret-right msl msr')); $tags = phutil_implode_html($caret, $tags); $view->addProperty(pht('Extends'), $tags); } $implements = $this->getImplementsLineage($symbol); if ($implements) { $items = array(); foreach ($implements as $spec) { $via = $spec['via']; $iface = $spec['interface']; if ($via == $symbol) { $items[] = $this->renderAtomTag($iface); } else { $items[] = array( $this->renderAtomTag($iface), " \xE2\x97\x80 ", $this->renderAtomTag($via)); } } $view->addProperty( pht('Implements'), phutil_implode_html(phutil_tag('br'), $items)); } } private function renderAtomTag(DivinerLiveSymbol $symbol) { return id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_OBJECT) ->setName($symbol->getName()) ->setHref($symbol->getURI()); } private function getExtendsLineage(DivinerLiveSymbol $symbol) { foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $lineage = $this->getExtendsLineage($extends); $lineage[] = $extends; return $lineage; } } return array(); } private function getImplementsLineage(DivinerLiveSymbol $symbol) { $implements = array(); // Do these first so we get interfaces ordered from most to least specific. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'interface') { $implements[$extends->getName()] = array( 'interface' => $extends, 'via' => $symbol, ); } } // Now do parent interfaces. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $implements += $this->getImplementsLineage($extends); } } return $implements; } private function buildDefined( PHUIPropertyListView $view, DivinerLiveSymbol $symbol) { $atom = $symbol->getAtom(); $defined = $atom->getFile().':'.$atom->getLine(); $link = $symbol->getBook()->getConfig('uri.source'); if ($link) { $link = strtr( $link, array( '%%' => '%', '%f' => phutil_escape_uri($atom->getFile()), '%l' => phutil_escape_uri($atom->getLine()), )); $defined = phutil_tag( 'a', array( 'href' => $link, 'target' => '_blank', ), $defined); } $view->addProperty(pht('Defined'), $defined); } private function composeMethods(DivinerLiveSymbol $symbol) { $methods = $this->findMethods($symbol); if (!$methods) { return $methods; } foreach ($methods as $name => $method) { // Check for "@task" on each parent, to find the most recently declared // "@task". $task = null; foreach ($method['atoms'] as $key => $method_symbol) { $atom = $method_symbol->getAtom(); if ($atom->getDocblockMetaValue('task')) { $task = $atom->getDocblockMetaValue('task'); } } $methods[$name]['task'] = $task; // Set 'inherited' if this atom has no implementation of the method. if (last($method['implementations']) !== $symbol) { $methods[$name]['inherited'] = true; } else { $methods[$name]['inherited'] = false; } } return $methods; } private function findMethods(DivinerLiveSymbol $symbol) { $child_specs = array(); foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == DivinerAtom::TYPE_CLASS) { $child_specs = $this->findMethods($extends); } } foreach ($symbol->getChildren() as $child) { if ($child->getType() == DivinerAtom::TYPE_METHOD) { $name = $child->getName(); if (isset($child_specs[$name])) { $child_specs[$name]['atoms'][] = $child; $child_specs[$name]['implementations'][] = $symbol; } else { $child_specs[$name] = array( 'atoms' => array($child), 'defined' => $symbol, 'implementations' => array($symbol), ); } } } return $child_specs; } private function composeTasks(DivinerLiveSymbol $symbol) { $extends_task_specs = array(); foreach ($symbol->getExtends() as $extends) { $extends_task_specs += $this->composeTasks($extends); } $task_specs = array(); $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); if (strlen($tasks)) { $tasks = phutil_split_lines($tasks, $retain_endings = false); foreach ($tasks as $task) { list($name, $title) = explode(' ', $task, 2); $name = trim($name); $title = trim($title); $task_specs[$name] = array( 'name' => $name, 'title' => $title, 'defined' => $symbol, ); } } $specs = $task_specs + $extends_task_specs; // Reorder "@tasks" in original declaration order. Basically, we want to // use the documentation of the closest subclass, but put tasks which // were declared by parents first. $keys = array_keys($extends_task_specs); $specs = array_select_keys($specs, $keys) + $specs; return $specs; } private function renderFullSignature( DivinerLiveSymbol $symbol, $is_link = false) { switch ($symbol->getType()) { case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_INTERFACE: case DivinerAtom::TYPE_METHOD: case DivinerAtom::TYPE_FUNCTION: break; default: return null; } $atom = $symbol->getAtom(); $out = array(); if ($atom->getProperty('final')) { $out[] = 'final'; } if ($atom->getProperty('abstract')) { $out[] = 'abstract'; } if ($atom->getProperty('access')) { $out[] = $atom->getProperty('access'); } if ($atom->getProperty('static')) { $out[] = 'static'; } switch ($symbol->getType()) { case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_INTERFACE: $out[] = $symbol->getType(); break; case DivinerAtom::TYPE_FUNCTION: switch ($atom->getLanguage()) { case 'php': $out[] = $symbol->getType(); break; } break; case DivinerAtom::TYPE_METHOD: switch ($atom->getLanguage()) { case 'php': $out[] = DivinerAtom::TYPE_FUNCTION; break; } break; } $anchor = null; switch ($symbol->getType()) { case DivinerAtom::TYPE_METHOD: $anchor = $symbol->getType().'/'.$symbol->getName(); break; default: break; } $out[] = phutil_tag( $anchor ? 'a' : 'span', array( 'class' => 'diviner-atom-signature-name', 'href' => $anchor ? '#'.$anchor : null, 'name' => $is_link ? null : $anchor, ), $symbol->getName()); $out = phutil_implode_html(' ', $out); $parameters = $atom->getProperty('parameters'); if ($parameters !== null) { $pout = array(); foreach ($parameters as $parameter) { $pout[] = idx($parameter, 'name', '...'); } $out = array($out, '('.implode(', ', $pout).')'); } return phutil_tag( 'span', array( 'class' => 'diviner-atom-signature', ), $out); } private function buildParametersAndReturn(array $symbols) { assert_instances_of($symbols, 'DivinerLiveSymbol'); $symbols = array_reverse($symbols); $out = array(); $collected_parameters = null; foreach ($symbols as $symbol) { $parameters = $symbol->getAtom()->getProperty('parameters'); if ($parameters !== null) { if ($collected_parameters === null) { $collected_parameters = array(); } foreach ($parameters as $key => $parameter) { if (isset($collected_parameters[$key])) { $collected_parameters[$key] += $parameter; } else { $collected_parameters[$key] = $parameter; } } } } if (nonempty($parameters)) { $out[] = id(new DivinerParameterTableView()) ->setHeader(pht('Parameters')) ->setParameters($parameters); } $collected_return = null; foreach ($symbols as $symbol) { $return = $symbol->getAtom()->getProperty('return'); if ($return) { if ($collected_return) { $collected_return += $return; } else { $collected_return = $return; } } } if (nonempty($return)) { $out[] = id(new DivinerReturnTableView()) ->setHeader(pht('Return')) ->setReturn($collected_return); } return $out; } private function renderDocumentationText( DivinerLiveSymbol $symbol, PhabricatorMarkupEngine $engine) { $field = 'default'; $content = $engine->getOutput($symbol, $field); if (strlen(trim($symbol->getMarkupText($field)))) { $content = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $content); } else { $atom = $symbol->getAtom(); $content = phutil_tag( 'div', array( 'class' => 'diviner-message-not-documented', ), DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType())); } return $content; } private function renderMethodDocumentationText( DivinerLiveSymbol $parent, array $spec, PhabricatorMarkupEngine $engine) { $symbols = array_values($spec['atoms']); $implementations = array_values($spec['implementations']); $field = 'default'; $out = array(); foreach ($symbols as $key => $symbol) { $impl = $implementations[$key]; if ($impl !== $parent) { if (!strlen(trim($symbol->getMarkupText($field)))) { continue; } } $doc = $this->renderDocumentationText($symbol, $engine); if (($impl !== $parent) || $out) { $where = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM_LEFT) ->addPadding(PHUI::PADDING_MEDIUM_RIGHT) ->addClass('diviner-method-implementation-header') ->appendChild($impl->getName()); $doc = array($where, $doc); if ($impl !== $parent) { $doc = phutil_tag( 'div', array( 'class' => 'diviner-method-implementation-inherited', ), $doc); } } $out[] = $doc; } // If we only have inherited implementations but none have documentation, // render the last one here so we get the "this thing has no documentation" // element. if (!$out) { $out[] = $this->renderDocumentationText($symbol, $engine); } return $out; } } diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php index a3aeaedfc..0ab122e80 100644 --- a/src/applications/diviner/controller/DivinerBookController.php +++ b/src/applications/diviner/controller/DivinerBookController.php @@ -1,102 +1,101 @@ bookName = $data['book']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($book->getShortTitle()) - ->setHref('/book/'.$book->getName().'/')); + $crumbs->addTextCrumb( + $book->getShortTitle(), + '/book/'.$book->getName().'/'); $header = id(new PHUIHeaderView()) ->setHeader($book->getTitle()) ->setUser($viewer) ->setPolicyObject($book); $document = new PHUIDocumentView(); $document->setHeader($header); $properties = $this->buildPropertyList($book); $atoms = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->execute(); $atoms = msort($atoms, 'getSortKey'); $group_spec = $book->getConfig('groups'); if (!is_array($group_spec)) { $group_spec = array(); } $groups = mgroup($atoms, 'getGroupName'); $groups = array_select_keys($groups, array_keys($group_spec)) + $groups; if (isset($groups[''])) { $no_group = $groups['']; unset($groups['']); $groups[''] = $no_group; } $out = array(); foreach ($groups as $group => $atoms) { $group_name = $book->getGroupName($group); $section = id(new DivinerSectionView()) ->setHeader($group_name); $section->addContent($this->renderAtomList($atoms)); $out[] = $section; } $document->appendChild($properties); $document->appendChild($out); return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $book->getTitle(), 'device' => true, )); } private function buildPropertyList(DivinerLiveBook $book) { $user = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($user); $policies = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $book); $view->addProperty( pht('Updated'), phabricator_datetime($book->getDateModified(), $user)); return $view; } } diff --git a/src/applications/diviner/controller/DivinerLegacyController.php b/src/applications/diviner/controller/DivinerLegacyController.php index b38926b63..1efdb8c5d 100644 --- a/src/applications/diviner/controller/DivinerLegacyController.php +++ b/src/applications/diviner/controller/DivinerLegacyController.php @@ -1,67 +1,65 @@ array( 'name' => 'Phabricator Ducks', 'flavor' => 'Oops, that should say "Docs".', ), 'http://www.phabricator.com/docs/arcanist/' => array( 'name' => 'Arcanist Docs', 'flavor' => 'Words have never been so finely crafted.', ), 'http://www.phabricator.com/docs/libphutil/' => array( 'name' => 'libphutil Docs', 'flavor' => 'Soothing prose; seductive poetry.', ), 'http://www.phabricator.com/docs/javelin/' => array( 'name' => 'Javelin Docs', 'flavor' => 'O, what noble scribe hath penned these words?', ), ); $request = $this->getRequest(); $viewer = $request->getUser(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setPlain(true); foreach ($links as $href => $link) { $item = id(new PHUIObjectItemView()) ->setHref($href) ->setHeader($link['name']) ->addAttribute($link['flavor']); $list->addItem($item); } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Documentation'))); + $crumbs->addTextCrumb(pht('Documentation')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Documentation')); $document = new PHUIDocumentView(); $document->setHeader($header); $document->appendChild($list); return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => pht('Documentation'), 'device' => true, )); } } diff --git a/src/applications/drydock/controller/DrydockBlueprintEditController.php b/src/applications/drydock/controller/DrydockBlueprintEditController.php index 9b2d2cb18..976640875 100644 --- a/src/applications/drydock/controller/DrydockBlueprintEditController.php +++ b/src/applications/drydock/controller/DrydockBlueprintEditController.php @@ -1,122 +1,118 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->id) { $blueprint = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$blueprint) { return new Aphront404Response(); } } else { $blueprint = new DrydockBlueprint(); } if ($request->isFormPost()) { $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); // TODO: Should we use transactions here? $blueprint->setViewPolicy($v_view_policy); $blueprint->setEditPolicy($v_edit_policy); $blueprint->save(); return id(new AphrontRedirectResponse()) ->setURI('/drydock/blueprint/'); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($blueprint) ->execute(); if ($request->isAjax()) { $form = id(new PHUIFormLayoutView()) ->setUser($viewer); } else { $form = id(new AphrontFormView()) ->setUser($viewer); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('className') ->setLabel(pht('Implementation')) ->setValue($blueprint->getClassName()) ->setDisabled(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($blueprint) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($blueprint) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)); $crumbs = $this->buildApplicationCrumbs(); $title = pht('Edit Blueprint'); $header = pht('Edit Blueprint %d', $blueprint->getID()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Blueprint %d', $blueprint->getID()))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); + $crumbs->addTextCrumb(pht('Edit')); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) ->appendChild($form) ->addSubmitButton(pht('Edit Blueprint')) ->addCancelButton($this->getApplicationURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($this->getApplicationURI())); $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/drydock/controller/DrydockBlueprintListController.php b/src/applications/drydock/controller/DrydockBlueprintListController.php index 3c18e98d1..af0594768 100644 --- a/src/applications/drydock/controller/DrydockBlueprintListController.php +++ b/src/applications/drydock/controller/DrydockBlueprintListController.php @@ -1,77 +1,74 @@ getRequest(); $user = $request->getUser(); $title = pht('Blueprints'); $blueprint_header = id(new PHUIHeaderView()) ->setHeader($title); $blueprints = id(new DrydockBlueprintQuery()) ->setViewer($user) ->execute(); $blueprint_list = $this->buildBlueprintListView($blueprints); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('New Blueprint')) ->setHref($this->getApplicationURI('blueprint/create/')) ->setIcon('create')); $nav = $this->buildSideNav('blueprint'); $nav->setCrumbs($crumbs); $nav->appendChild( array( $blueprint_header, $blueprint_list )); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } protected function buildBlueprintListView(array $blueprints) { assert_instances_of($blueprints, 'DrydockBlueprint'); $user = $this->getRequest()->getUser(); $view = new PHUIObjectItemListView(); foreach ($blueprints as $blueprint) { $item = id(new PHUIObjectItemView()) ->setHeader($blueprint->getClassName()) ->setHref($this->getApplicationURI('/blueprint/'.$blueprint->getID())) ->setObjectName(pht('Blueprint %d', $blueprint->getID())); if ($blueprint->getImplementation()->isEnabled()) { $item->addAttribute(pht('Enabled')); $item->setBarColor('green'); } else { $item->addAttribute(pht('Disabled')); $item->setBarColor('red'); } $item->addAttribute($blueprint->getImplementation()->getDescription()); $view->addItem($item); } return $view; } } diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php index 44e21a116..bed26299f 100644 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -1,100 +1,98 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $blueprint = id(new DrydockBlueprint())->load($this->id); if (!$blueprint) { return new Aphront404Response(); } $title = 'Blueprint '.$blueprint->getID().' '.$blueprint->getClassName(); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = $this->buildActionListView($blueprint); $properties = $this->buildPropertyListView($blueprint, $actions); $blueprint_uri = 'blueprint/'.$blueprint->getID().'/'; $blueprint_uri = $this->getApplicationURI($blueprint_uri); $resources = id(new DrydockResourceQuery()) ->withBlueprintPHIDs(array($blueprint->getPHID())) ->setViewer($user) ->execute(); $resource_list = $this->buildResourceListView($resources); $resource_list->setNoDataString(pht('This blueprint has no resources.')); $pager = new AphrontPagerView(); $pager->setURI(new PhutilURI($blueprint_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Blueprint %d', $blueprint->getID()))); + $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $resource_list ), array( 'device' => true, 'title' => $title, )); } private function buildActionListView(DrydockBlueprint $blueprint) { $view = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($blueprint); $uri = '/blueprint/edit/'.$blueprint->getID().'/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) ->setName(pht('Edit Blueprint Policies')) ->setIcon('edit') ->setWorkflow(true) ->setDisabled(false)); return $view; } private function buildPropertyListView( DrydockBlueprint $blueprint, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); $view->addProperty( pht('Implementation'), $blueprint->getClassName()); return $view; } } diff --git a/src/applications/drydock/controller/DrydockLeaseListController.php b/src/applications/drydock/controller/DrydockLeaseListController.php index 8ae5e4add..791299ff4 100644 --- a/src/applications/drydock/controller/DrydockLeaseListController.php +++ b/src/applications/drydock/controller/DrydockLeaseListController.php @@ -1,49 +1,46 @@ getRequest(); $user = $request->getUser(); $nav = $this->buildSideNav('lease'); $pager = new AphrontPagerView(); $pager->setURI(new PhutilURI('/drydock/lease/'), 'offset'); $pager->setOffset($request->getInt('offset')); $leases = id(new DrydockLeaseQuery()) ->needResources(true) ->executeWithOffsetPager($pager); $title = pht('Leases'); $header = id(new PHUIHeaderView()) ->setHeader($title); $lease_list = $this->buildLeaseListView($leases); $nav->appendChild( array( $header, $lease_list, $pager, )); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'device' => true, 'title' => $title, )); } } diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index 78f5447c3..4739dfdbc 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -1,137 +1,134 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $lease = id(new DrydockLease())->load($this->id); if (!$lease) { return new Aphront404Response(); } $lease_uri = $this->getApplicationURI('lease/'.$lease->getID().'/'); $title = pht('Lease %d', $lease->getID()); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = $this->buildActionListView($lease); $properties = $this->buildPropertyListView($lease, $actions); $pager = new AphrontPagerView(); $pager->setURI(new PhutilURI($lease_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) ->withLeaseIDs(array($lease->getID())) ->executeWithOffsetPager($pager); $log_table = $this->buildLogTableView($logs); $log_table->appendChild($pager); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($lease_uri)); + $crumbs->addTextCrumb($title, $lease_uri); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $log_table, ), array( 'device' => true, 'title' => $title, )); } private function buildActionListView(DrydockLease $lease) { $view = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($lease); $id = $lease->getID(); $can_release = ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Release Lease')) ->setIcon('delete') ->setHref($this->getApplicationURI("/lease/{$id}/release/")) ->setWorkflow(true) ->setDisabled(!$can_release)); return $view; } private function buildPropertyListView( DrydockLease $lease, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_ACTIVE: $status = pht('Active'); break; case DrydockLeaseStatus::STATUS_RELEASED: $status = pht('Released'); break; case DrydockLeaseStatus::STATUS_EXPIRED: $status = pht('Expired'); break; case DrydockLeaseStatus::STATUS_PENDING: $status = pht('Pending'); break; case DrydockLeaseStatus::STATUS_BROKEN: $status = pht('Broken'); break; default: $status = pht('Unknown'); break; } $view->addProperty( pht('Status'), $status); $view->addProperty( pht('Resource Type'), $lease->getResourceType()); $view->addProperty( pht('Resource'), $lease->getResourceID()); $attributes = $lease->getAttributes(); if ($attributes) { $view->addSectionHeader(pht('Attributes')); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } } diff --git a/src/applications/drydock/controller/DrydockLogController.php b/src/applications/drydock/controller/DrydockLogController.php index 1a378df3c..73907468b 100644 --- a/src/applications/drydock/controller/DrydockLogController.php +++ b/src/applications/drydock/controller/DrydockLogController.php @@ -1,61 +1,58 @@ getRequest(); $user = $request->getUser(); $nav = $this->buildSideNav('log'); $query = new DrydockLogQuery(); $resource_ids = $request->getStrList('resource'); if ($resource_ids) { $query->withResourceIDs($resource_ids); } $lease_ids = $request->getStrList('lease'); if ($lease_ids) { $query->withLeaseIDs($lease_ids); } $pager = new AphrontPagerView(); $pager->setPageSize(500); $pager->setOffset($request->getInt('offset')); $pager->setURI($request->getRequestURI(), 'offset'); $logs = $query->executeWithOffsetPager($pager); $title = pht('Logs'); $header = id(new PHUIHeaderView()) ->setHeader($title); $table = $this->buildLogTableView($logs); $table->appendChild($pager); $nav->appendChild( array( $header, $table, $pager, )); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI('/logs/'))); + $crumbs->addTextCrumb($title, $this->getApplicationURI('/logs/')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/drydock/controller/DrydockResourceListController.php b/src/applications/drydock/controller/DrydockResourceListController.php index f93e343aa..a868eefc2 100644 --- a/src/applications/drydock/controller/DrydockResourceListController.php +++ b/src/applications/drydock/controller/DrydockResourceListController.php @@ -1,46 +1,43 @@ getRequest(); $user = $request->getUser(); $title = pht('Resources'); $resource_header = id(new PHUIHeaderView()) ->setHeader($title); $pager = new AphrontPagerView(); $pager->setURI(new PhutilURI('/drydock/resource/'), 'offset'); $resources = id(new DrydockResourceQuery()) ->setViewer($user) ->executeWithOffsetPager($pager); $resource_list = $this->buildResourceListView($resources); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $nav = $this->buildSideNav('resource'); $nav->setCrumbs($crumbs); $nav->appendChild( array( $resource_header, $resource_list, $pager, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index 242129632..46ecc1a4c 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -1,129 +1,127 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $resource = id(new DrydockResource())->load($this->id); if (!$resource) { return new Aphront404Response(); } $title = 'Resource '.$resource->getID().' '.$resource->getName(); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = $this->buildActionListView($resource); $properties = $this->buildPropertyListView($resource, $actions); $resource_uri = 'resource/'.$resource->getID().'/'; $resource_uri = $this->getApplicationURI($resource_uri); $leases = id(new DrydockLeaseQuery()) ->withResourceIDs(array($resource->getID())) ->needResources(true) ->execute(); $lease_list = $this->buildLeaseListView($leases); $lease_list->setNoDataString(pht('This resource has no leases.')); $pager = new AphrontPagerView(); $pager->setURI(new PhutilURI($resource_uri), 'offset'); $pager->setOffset($request->getInt('offset')); $logs = id(new DrydockLogQuery()) ->withResourceIDs(array($resource->getID())) ->executeWithOffsetPager($pager); $log_table = $this->buildLogTableView($logs); $log_table->appendChild($pager); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Resource %d', $resource->getID()))); + $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $lease_list, $log_table, ), array( 'device' => true, 'title' => $title, )); } private function buildActionListView(DrydockResource $resource) { $view = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($resource); $can_close = ($resource->getStatus() == DrydockResourceStatus::STATUS_OPEN); $uri = '/resource/'.$resource->getID().'/close/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) ->setName(pht('Close Resource')) ->setIcon('delete') ->setWorkflow(true) ->setDisabled(!$can_close)); return $view; } private function buildPropertyListView( DrydockResource $resource, PhabricatorActionListView $actions) { $view = new PHUIPropertyListView(); $view->setActionList($actions); $status = $resource->getStatus(); $status = DrydockResourceStatus::getNameForStatus($status); $view->addProperty( pht('Status'), $status); $view->addProperty( pht('Resource Type'), $resource->getType()); // TODO: Load handle. $view->addProperty( pht('Blueprint'), $resource->getBlueprintPHID()); $attributes = $resource->getAttributes(); if ($attributes) { $view->addSectionHeader(pht('Attributes')); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } } return $view; } } diff --git a/src/applications/feed/controller/PhabricatorFeedDetailController.php b/src/applications/feed/controller/PhabricatorFeedDetailController.php index 9b43f150b..98ee7e2f6 100644 --- a/src/applications/feed/controller/PhabricatorFeedDetailController.php +++ b/src/applications/feed/controller/PhabricatorFeedDetailController.php @@ -1,49 +1,47 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $story = id(new PhabricatorFeedQuery()) ->setViewer($user) ->withChronologicalKeys(array($this->id)) ->executeOne(); if (!$story) { return new Aphront404Response(); } $feed = array($story); $builder = new PhabricatorFeedBuilder($feed); $builder->setUser($user); $feed_view = $builder->buildView(); $title = pht('Story'); $feed_view = phutil_tag_div('phabricator-feed-frame', $feed_view); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $feed_view, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 0f7fe42d5..b224c9b55 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -1,273 +1,272 @@ phid = $data['phid']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $phid = $file->getPHID(); $xactions = id(new PhabricatorFileTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($phid)) ->execute(); $handle_phids = array_merge( array($file->getAuthorPHID()), $file->getObjectPHIDs()); $this->loadHandles($handle_phids); $header = id(new PHUIHeaderView()) ->setHeader($file->getName()); $ttl = $file->getTTL(); if ($ttl !== null) { $ttl_tag = id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_OBJECT) ->setName(pht("Temporary")); $header->addTag($ttl_tag); } $actions = $this->buildActionView($file); $timeline = $this->buildTransactionView($file, $xactions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('F'.$file->getID()) - ->setHref($this->getApplicationURI("/info/{$phid}/"))); + $crumbs->addTextCrumb( + 'F'.$file->getID(), + $this->getApplicationURI("/info/{$phid}/")); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header); $this->buildPropertyViews($object_box, $file, $actions); return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline ), array( 'title' => $file->getName(), 'device' => true, 'pageObjects' => array($file->getPHID()), )); } private function buildTransactionView( PhabricatorFile $file, array $xactions) { $user = $this->getRequest()->getUser(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($file->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Question File Integrity'); $submit_button_name = $is_serious ? pht('Add Comment') : pht('Debate the Bits'); $draft = PhabricatorDraft::newFromUserAndKey($user, $file->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($file->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/')) ->setSubmitButtonName($submit_button_name); return array( $timeline, $add_comment_form); } private function buildActionView(PhabricatorFile $file) { $request = $this->getRequest(); $user = $request->getUser(); $id = $file->getID(); $view = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($file); if ($file->isViewableInBrowser()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View File')) ->setIcon('preview') ->setHref($file->getViewURI())); } else { $view->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setRenderAsForm(true) ->setDownload(true) ->setName(pht('Download File')) ->setIcon('download') ->setHref($file->getViewURI())); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete File')) ->setIcon('delete') ->setHref($this->getApplicationURI("/delete/{$id}/")) ->setWorkflow(true)); return $view; } private function buildPropertyViews( PHUIObjectBoxView $box, PhabricatorFile $file, PhabricatorActionListView $actions) { $request = $this->getRequest(); $user = $request->getUser(); $properties = id(new PHUIPropertyListView()); $properties->setActionList($actions); $box->addPropertyList($properties, pht('Details')); if ($file->getAuthorPHID()) { $properties->addProperty( pht('Author'), $this->getHandle($file->getAuthorPHID())->renderLink()); } $properties->addProperty( pht('Created'), phabricator_datetime($file->getDateCreated(), $user)); $finfo = id(new PHUIPropertyListView()); $box->addPropertyList($finfo, pht('File Info')); $finfo->addProperty( pht('Size'), phabricator_format_bytes($file->getByteSize())); $finfo->addProperty( pht('Mime Type'), $file->getMimeType()); $width = $file->getImageWidth(); if ($width) { $finfo->addProperty( pht('Width'), pht('%s px', new PhutilNumber($width))); } $height = $file->getImageHeight(); if ($height) { $finfo->addProperty( pht('Height'), pht('%s px', new PhutilNumber($height))); } $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); $storage_properties->addProperty( pht('Engine'), $file->getStorageEngine()); $storage_properties->addProperty( pht('Format'), $file->getStorageFormat()); $storage_properties->addProperty( pht('Handle'), $file->getStorageHandle()); $phids = $file->getObjectPHIDs(); if ($phids) { $attached = new PHUIPropertyListView(); $box->addPropertyList($attached, pht('Attached')); $attached->addProperty( pht('Attached To'), $this->renderHandlesForPHIDs($phids)); } if ($file->isViewableImage()) { $image = phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phui-property-list-image', )); $linked_image = phutil_tag( 'a', array( 'href' => $file->getViewURI(), ), $image); $media = id(new PHUIPropertyListView()) ->addImageContent($linked_image); $box->addPropertyList($media); } else if ($file->isAudio()) { $audio = phutil_tag( 'audio', array( 'controls' => 'controls', 'class' => 'phui-property-list-audio', ), phutil_tag( 'source', array( 'src' => $file->getViewURI(), 'type' => $file->getMimeType(), ))); $media = id(new PHUIPropertyListView()) ->addImageContent($audio); $box->addPropertyList($media); } } } diff --git a/src/applications/files/controller/PhabricatorFileUploadController.php b/src/applications/files/controller/PhabricatorFileUploadController.php index b92bd9ac0..22d5846e4 100644 --- a/src/applications/files/controller/PhabricatorFileUploadController.php +++ b/src/applications/files/controller/PhabricatorFileUploadController.php @@ -1,117 +1,114 @@ getRequest(); $user = $request->getUser(); $e_file = true; $errors = array(); if ($request->isFormPost()) { if (!$request->getFileExists('file')) { $e_file = pht('Required'); $errors[] = pht('You must select a file to upload.'); } else { $file = PhabricatorFile::newFromPHPUpload( idx($_FILES, 'file'), array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), 'isExplicitUpload' => true, )); } if (!$errors) { return id(new AphrontRedirectResponse())->setURI($file->getViewURI()); } } $support_id = celerity_generate_unique_node_id(); $instructions = id(new AphrontFormMarkupControl()) ->setControlID($support_id) ->setControlStyle('display: none') ->setValue(hsprintf( '

    %s %s

    ', pht('Drag and Drop:'), pht( 'You can also upload files by dragging and dropping them from your '. 'desktop onto this page or the Phabricator home page.'))); $form = id(new AphrontFormView()) ->setUser($user) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file') ->setError($e_file) ->setCaption($this->renderUploadLimit())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($request->getStr('name')) ->setCaption(pht('Optional file display name.'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload')) ->addCancelButton('/file/')) ->appendChild($instructions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Upload')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Upload'), $request->getRequestURI()); $title = pht('Upload File'); if ($errors) { $errors = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $global_upload = id(new PhabricatorGlobalUploadTargetView()) ->setShowIfSupportedID($support_id); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $global_upload, ), array( 'title' => $title, 'device' => true, )); } private function renderUploadLimit() { $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); $limit = phabricator_parse_bytes($limit); if ($limit) { $formatted = phabricator_format_bytes($limit); return 'Maximum file size: '.$formatted; } $doc_href = PhabricatorEnv::getDocLink( 'article/Configuring_File_Upload_Limits.html'); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), 'Configuring File Upload Limits'); return hsprintf('Upload limit is not configured, see %s.', $doc_link); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 8029b6fc3..09d5f7ec2 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -1,302 +1,300 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$build) { return new Aphront404Response(); } $title = pht("Build %d", $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($build); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($build); $this->buildPropertyLists($box, $build, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->withBuildPHIDs(array($build->getPHID())) ->execute(); $targets = array(); foreach ($build_targets as $build_target) { $header = id(new PHUIHeaderView()) ->setHeader(pht( 'Build Target %d (%s)', $build_target->getID(), $build_target->getImplementation()->getName())) ->setUser($viewer); $properties = new PHUIPropertyListView(); $details = $build_target->getDetails(); if ($details) { $properties->addSectionHeader(pht('Configuration Details')); foreach ($details as $key => $value) { $properties->addProperty($key, $value); } } $variables = $build_target->getVariables(); if ($variables) { $properties->addSectionHeader(pht('Variables')); foreach ($variables as $key => $value) { $properties->addProperty($key, $value); } } $targets[] = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $targets[] = $this->buildArtifacts($build_target); $targets[] = $this->buildLog($build, $build_target); } return $this->buildApplicationPage( array( $crumbs, $box, $targets ), array( 'title' => $title, 'device' => true, )); } private function buildArtifacts(HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); if (count($artifacts) === 0) { return null; } $list = new PHUIObjectItemListView(); foreach ($artifacts as $artifact) { $list->addItem($artifact->getObjectItemView($viewer)); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Build Artifacts')) ->setUser($viewer); $box = id(new PHUIObjectBoxView()) ->setHeader($header); return array($box, $list); } private function buildLog( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $limit = $request->getInt('l', 25); $logs = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); $log_boxes = array(); foreach ($logs as $log) { $start = 1; $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); if ($limit !== 0) { $start = count($lines) - $limit; if ($start >= 1) { $lines = array_slice($lines, -$limit, $limit); } else { $start = 1; } } $log_view = new ShellLogView(); $log_view->setLines($lines); $log_view->setStart($start); $header = id(new PHUIHeaderView()) ->setHeader(pht( 'Build Log %d (%s - %s)', $log->getID(), $log->getLogSource(), $log->getLogType())) ->setSubheader($this->createLogHeader($build, $log)) ->setUser($viewer); $log_boxes[] = id(new PHUIObjectBoxView()) ->setHeader($header) ->setForm($log_view); } return $log_boxes; } private function createLogHeader($build, $log) { $request = $this->getRequest(); $limit = $request->getInt('l', 25); $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); $lines_100 = $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); if ($limit === 25) { $link_25 = phutil_tag('strong', array(), $link_25); } else if ($limit === 50) { $link_50 = phutil_tag('strong', array(), $link_50); } else if ($limit === 100) { $link_100 = phutil_tag('strong', array(), $link_100); } else if ($limit === 0) { $link_0 = phutil_tag('strong', array(), $link_0); } return phutil_tag( 'span', array(), array( $link_25, ' - ', $link_50, ' - ', $link_100, ' - ', $link_0, ' Lines')); } private function buildActionList(HarbormasterBuild $build) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $build->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($build) ->setObjectURI("/build/{$id}"); $action = id(new PhabricatorActionView()) ->setName(pht('Cancel Build')) ->setIcon('delete'); switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_PENDING: case HarbormasterBuild::STATUS_WAITING: case HarbormasterBuild::STATUS_BUILDING: $cancel_uri = $this->getApplicationURI('/build/cancel/'.$id.'/'); $action ->setHref($cancel_uri) ->setWorkflow(true); break; default: $action ->setDisabled(true); break; } $list->addAction($action); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuild $build, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($build) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Status'), $this->getStatus($build)); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array( $build->getBuildablePHID(), $build->getBuildPlanPHID())) ->execute(); $properties->addProperty( pht('Buildable'), $handles[$build->getBuildablePHID()]->renderLink()); $properties->addProperty( pht('Build Plan'), $handles[$build->getBuildPlanPHID()]->renderLink()); } private function getStatus(HarbormasterBuild $build) { if ($build->getCancelRequested()) { return pht('Cancelling'); } switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_INACTIVE: return pht('Inactive'); case HarbormasterBuild::STATUS_PENDING: return pht('Pending'); case HarbormasterBuild::STATUS_WAITING: return pht('Waiting'); case HarbormasterBuild::STATUS_BUILDING: return pht('Building'); case HarbormasterBuild::STATUS_PASSED: return pht('Passed'); case HarbormasterBuild::STATUS_FAILED: return pht('Failed'); case HarbormasterBuild::STATUS_ERROR: return pht('Unexpected Error'); case HarbormasterBuild::STATUS_CANCELLED: return pht('Cancelled'); default: return pht('Unknown'); } } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php b/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php index 1bfdf5993..3c27fb8ac 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableEditController.php @@ -1,147 +1,140 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); if ($this->id) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } } else { $buildable = HarbormasterBuildable::initializeNewBuildable($viewer); } $e_name = true; $v_name = null; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('buildablePHID'); if ($v_name) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($v_name)) ->executeOne(); if ($object instanceof DifferentialRevision) { $revision = $object; $object = $object->loadActiveDiff(); $buildable ->setBuildablePHID($object->getPHID()) ->setContainerPHID($revision->getPHID()); } else if ($object instanceof PhabricatorRepositoryCommit) { $buildable ->setBuildablePHID($object->getPHID()) ->setContainerPHID($object->getRepository()->getPHID()); } else { $e_name = pht('Invalid'); $errors[] = pht('Enter the name of a revision or commit.'); } } else { $e_name = pht('Required'); $errors[] = pht('You must choose a revision or commit to build.'); } if (!$errors) { $buildable->save(); $buildable_uri = '/B'.$buildable->getID(); return id(new AphrontRedirectResponse())->setURI($buildable_uri); } } if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $is_new = (!$buildable->getID()); if ($is_new) { $title = pht('New Buildable'); $cancel_uri = $this->getApplicationURI(); $save_button = pht('Create Buildable'); } else { $id = $buildable->getID(); $title = pht('Edit Buildable'); $cancel_uri = "/B{$id}"; $save_button = pht('Save Buildable'); } $form = id(new AphrontFormView()) ->setUser($viewer); if ($is_new) { $form ->appendRemarkupInstructions( pht( 'Enter the name of a commit or revision, like `rX123456789` '. 'or `D123`.')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Buildable Name') ->setName('buildablePHID') ->setError($e_name) ->setValue($v_name)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Buildable')) ->setValue($buildable->getBuildableHandle()->renderLink())); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue($save_button) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Buildable'))); + $crumbs->addTextCrumb(pht('New Buildable')); } else { $id = $buildable->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("B{$id}") - ->setHref("/B{$id}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb("B{$id}", "/B{$id}"); + $crumbs->addTextCrumb(pht('Edit')); } return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index 758d8b28d..41e5ad747 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -1,160 +1,158 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needBuildableHandles(true) ->needContainerHandles(true) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($buildable->getPHID())) ->execute(); $build_list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($builds as $build) { $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Build %d', $build->getID())) ->setHeader($build->getName()) ->setHref($view_uri); if ($build->getCancelRequested()) { $item->setBarColor('black'); $item->addAttribute(pht('Cancelling')); } else { switch ($build->getBuildStatus()) { case HarbormasterBuild::STATUS_INACTIVE: $item->setBarColor('grey'); $item->addAttribute(pht('Inactive')); break; case HarbormasterBuild::STATUS_PENDING: $item->setBarColor('blue'); $item->addAttribute(pht('Pending')); break; case HarbormasterBuild::STATUS_WAITING: $item->setBarColor('violet'); $item->addAttribute(pht('Waiting')); break; case HarbormasterBuild::STATUS_BUILDING: $item->setBarColor('yellow'); $item->addAttribute(pht('Building')); break; case HarbormasterBuild::STATUS_PASSED: $item->setBarColor('green'); $item->addAttribute(pht('Passed')); break; case HarbormasterBuild::STATUS_FAILED: $item->setBarColor('red'); $item->addAttribute(pht('Failed')); break; case HarbormasterBuild::STATUS_ERROR: $item->setBarColor('red'); $item->addAttribute(pht('Unexpected Error')); break; case HarbormasterBuild::STATUS_CANCELLED: $item->setBarColor('black'); $item->addAttribute(pht('Cancelled')); break; } } $build_list->addItem($item); } $title = pht("Buildable %d", $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($buildable); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($buildable); $this->buildPropertyLists($box, $buildable, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("B{$id}")); + $crumbs->addTextCrumb("B{$id}"); return $this->buildApplicationPage( array( $crumbs, $box, $build_list, ), array( 'title' => $title, 'device' => true, )); } private function buildActionList(HarbormasterBuildable $buildable) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $buildable->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($buildable) ->setObjectURI("/B{$id}"); $apply_uri = $this->getApplicationURI('/buildable/apply/'.$id.'/'); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Apply Build Plan')) ->setIcon('edit') ->setHref($apply_uri) ->setWorkflow(true)); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildable $buildable, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($buildable) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Buildable'), $buildable->getBuildableHandle()->renderLink()); if ($buildable->getContainerHandle() !== null) { $properties->addProperty( pht('Container'), $buildable->getContainerHandle()->renderLink()); } } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanController.php b/src/applications/harbormaster/controller/HarbormasterPlanController.php index 6ba3d8c0b..bc42e7fed 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanController.php @@ -1,16 +1,15 @@ addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Build Plans')) - ->setHref($this->getApplicationURI('plan/'))); + $crumbs->addTextCrumb( + pht('Build Plans'), + $this->getApplicationURI('plan/')); return $crumbs; } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php index bc58d4eef..95495e342 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanEditController.php @@ -1,121 +1,116 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); if ($this->id) { $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } } else { $plan = HarbormasterBuildPlan::initializeNewBuildPlan($viewer); } $e_name = true; $v_name = $plan->getName(); $validation_exception = null; if ($request->isFormPost()) { $xactions = array(); $v_name = $request->getStr('name'); $e_name = null; $type_name = HarbormasterBuildPlanTransaction::TYPE_NAME; $xactions[] = id(new HarbormasterBuildPlanTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $editor = id(new HarbormasterBuildPlanEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request); try { $editor->applyTransactions($plan, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('plan/'.$plan->getID().'/')); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $validation_exception->getShortMessage( HarbormasterBuildPlanTransaction::TYPE_NAME); } } $is_new = (!$plan->getID()); if ($is_new) { $title = pht('New Build Plan'); $cancel_uri = $this->getApplicationURI(); $save_button = pht('Create Build Plan'); } else { $id = $plan->getID(); $title = pht('Edit Build Plan'); $cancel_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); $save_button = pht('Save Build Plan'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Plan Name') ->setName('name') ->setError($e_name) ->setValue($v_name)); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue($save_button) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Build Plan'))); + $crumbs->addTextCrumb(pht('New Build Plan')); } else { $id = $plan->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id)) - ->setHref($this->getApplicationURI("plan/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb( + pht("Plan %d", $id), + $this->getApplicationURI("plan/{$id}/")); + $crumbs->addTextCrumb(pht('Edit')); } return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 36c896a06..920f3148f 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -1,213 +1,211 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $xactions = id(new HarbormasterBuildPlanTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($plan->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($plan->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $title = pht("Plan %d", $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($plan); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($plan); $this->buildPropertyLists($box, $plan, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id))); + $crumbs->addTextCrumb(pht("Plan %d", $id)); $step_list = $this->buildStepList($plan); return $this->buildApplicationPage( array( $crumbs, $box, $step_list, $xaction_view, ), array( 'title' => $title, 'device' => true, )); } private function buildStepList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $list_id = celerity_generate_unique_node_id(); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $can_edit = $this->hasApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); $i = 1; $step_list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setID($list_id); Javelin::initBehavior( 'harbormaster-reorder-steps', array( 'listID' => $list_id, 'orderURI' => '/harbormaster/plan/order/'.$plan->getID().'/', )); foreach ($steps as $step) { $implementation = null; try { $implementation = $step->getStepImplementation(); } catch (Exception $ex) { // We can't initialize the implementation. This might be because // it's been renamed or no longer exists. $item = id(new PHUIObjectItemView()) ->setObjectName("Step ".$i++) ->setHeader(pht('Unknown Implementation')) ->setBarColor('red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())) ->addAction( id(new PHUIListItemView()) ->setIcon('delete') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setRenderNameAsTooltip(true) ->setName(pht("Delete")) ->setHref( $this->getApplicationURI("step/delete/".$step->getID()."/"))); $step_list->addItem($item); continue; } $item = id(new PHUIObjectItemView()) ->setObjectName("Step ".$i++) ->setHeader($implementation->getName()); if (!$implementation->validateSettings()) { $item ->setBarColor('red') ->addAttribute(pht('This step is not configured correctly.')); } else { $item->addAttribute($implementation->getDescription()); } if ($can_edit) { $edit_uri = $this->getApplicationURI("step/edit/".$step->getID()."/"); $item ->setHref($edit_uri) ->addAction( id(new PHUIListItemView()) ->setIcon('delete') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setRenderNameAsTooltip(true) ->setName(pht("Delete")) ->setHref( $this->getApplicationURI("step/delete/".$step->getID()."/"))); $item->setGrippable(true); $item->addSigil('build-step'); $item->setMetadata( array( 'stepID' => $step->getID(), )); } $step_list->addItem($item); } return $step_list; } private function buildActionList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $plan->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($plan) ->setObjectURI($this->getApplicationURI("plan/{$id}/")); $can_edit = $this->hasApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Plan')) ->setHref($this->getApplicationURI("plan/edit/{$id}/")) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setIcon('edit')); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Build Step')) ->setHref($this->getApplicationURI("step/add/{$id}/")) ->setWorkflow($can_edit) ->setDisabled(!$can_edit) ->setIcon('new')); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildPlan $plan, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($plan) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Created'), phabricator_datetime($plan->getDateCreated(), $viewer)); } } diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 6b445c0bf..b141eabaf 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -1,179 +1,176 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); $step = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$step) { return new Aphront404Response(); } $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withPHIDs(array($step->getBuildPlanPHID())) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $implementation = $step->getStepImplementation(); $implementation->validateSettingDefinitions(); $settings = $implementation->getSettings(); $errors = array(); if ($request->isFormPost()) { foreach ($implementation->getSettingDefinitions() as $name => $opt) { $readable_name = $this->getReadableName($name, $opt); $value = $this->getValueFromRequest($request, $name, $opt['type']); // TODO: This won't catch any validation issues unless the field // is missing completely. How should we check if the user is // required to enter an integer? if ($value === null) { $errors[] = $readable_name.' is not valid.'; } else { $step->setDetail($name, $value); } } if (count($errors) === 0) { $step->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('plan/'.$plan->getID().'/')); } } $form = id(new AphrontFormView()) ->setUser($viewer); $instructions = $implementation->getSettingRemarkupInstructions(); if ($instructions !== null) { $form->appendRemarkupInstructions($instructions); } // We need to render out all of the fields for the settings that // the implementation has. foreach ($implementation->getSettingDefinitions() as $name => $opt) { if ($request->isFormPost()) { $value = $this->getValueFromRequest($request, $name, $opt['type']); } else { $value = $settings[$name]; } switch ($opt['type']) { case BuildStepImplementation::SETTING_TYPE_STRING: case BuildStepImplementation::SETTING_TYPE_INTEGER: $control = id(new AphrontFormTextControl()) ->setLabel($this->getReadableName($name, $opt)) ->setName($name) ->setValue($value); break; case BuildStepImplementation::SETTING_TYPE_BOOLEAN: $control = id(new AphrontFormCheckboxControl()) ->setLabel($this->getReadableName($name, $opt)) ->setName($name) ->setValue($value); break; case BuildStepImplementation::SETTING_TYPE_ARTIFACT: $filter = $opt['artifact_type']; $available_artifacts = BuildStepImplementation::loadAvailableArtifacts( $plan, $step, $filter); $options = array(); foreach ($available_artifacts as $key => $type) { $options[$key] = $key; } $control = id(new AphrontFormSelectControl()) ->setLabel($this->getReadableName($name, $opt)) ->setName($name) ->setValue($value) ->setOptions($options); break; default: throw new Exception("Unable to render field with unknown type."); } if (isset($opt['description'])) { $control->setCaption($opt['description']); } $form->appendChild($control); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Step Configuration')) ->addCancelButton( $this->getApplicationURI('plan/'.$plan->getID().'/'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText('Edit Step: '.$implementation->getName()) ->setValidationException(null) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $plan->getID(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Plan %d", $id)) - ->setHref($this->getApplicationURI("plan/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Step'))); + $crumbs->addTextCrumb( + pht("Plan %d", $id), + $this->getApplicationURI("plan/{$id}/")); + $crumbs->addTextCrumb(pht('Edit Step')); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $implementation->getName(), 'device' => true, )); } public function getReadableName($name, $opt) { $readable_name = $name; if (isset($opt['name'])) { $readable_name = $opt['name']; } return $readable_name; } public function getValueFromRequest(AphrontRequest $request, $name, $type) { switch ($type) { case BuildStepImplementation::SETTING_TYPE_STRING: case BuildStepImplementation::SETTING_TYPE_ARTIFACT: return $request->getStr($name); break; case BuildStepImplementation::SETTING_TYPE_INTEGER: return $request->getInt($name); break; case BuildStepImplementation::SETTING_TYPE_BOOLEAN: return $request->getBool($name); break; default: throw new Exception("Unsupported setting type '".$type."'."); } } } diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php index bb866d66e..7fac8d08a 100644 --- a/src/applications/herald/controller/HeraldNewController.php +++ b/src/applications/herald/controller/HeraldNewController.php @@ -1,107 +1,105 @@ contentType = idx($data, 'type'); $this->ruleType = idx($data, 'rule_type'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); if (empty($content_type_map[$this->contentType])) { $this->contentType = head_key($content_type_map); } $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); if (empty($rule_type_map[$this->ruleType])) { $this->ruleType = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; } // Reorder array to put "personal" first. $rule_type_map = array_select_keys( $rule_type_map, array( HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, )) + $rule_type_map; list($can_global, $global_link) = $this->explainApplicationCapability( HeraldCapabilityManageGlobalRules::CAPABILITY, pht('You have permission to create and manage global rules.'), pht('You do not have permission to create or manage global rules.')); $captions = array( HeraldRuleTypeConfig::RULE_TYPE_PERSONAL => pht( 'Personal rules notify you about events. You own them, but they can '. 'only affect you. Personal rules only trigger for objects you have '. 'permission to see.'), HeraldRuleTypeConfig::RULE_TYPE_GLOBAL => array( pht( 'Global rules notify anyone about events. Global rules can '. 'bypass access control policies and act on any object.'), $global_link, ), ); $radio = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Type')) ->setName('rule_type') ->setValue($this->ruleType); foreach ($rule_type_map as $value => $name) { $disabled = ($value == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) && (!$can_global); $radio->addButton( $value, $name, idx($captions, $value), $disabled ? 'disabled' : null, $disabled); } $form = id(new AphrontFormView()) ->setUser($user) ->setAction('/herald/edit/') ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('New Rule for')) ->setName('content_type') ->setValue($this->contentType) ->setOptions($content_type_map)) ->appendChild($radio) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Rule')) ->addCancelButton($this->getApplicationURI())); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Herald Rule')) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Rule'))); + ->addTextCrumb(pht('Create Rule')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create Herald Rule'), 'device' => true, )); } } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index 81810f7b9..85b8a6164 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,583 +1,581 @@ id = (int)idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); if ($this->id) { $id = $this->id; $rule = id(new HeraldRuleQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI("rule/{$id}/"); } else { $rule = new HeraldRule(); $rule->setAuthorPHID($user->getPHID()); $rule->setMustMatchAll(1); $content_type = $request->getStr('content_type'); $rule->setContentType($content_type); $rule_type = $request->getStr('rule_type'); if (!isset($rule_type_map[$rule_type])) { $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; } $rule->setRuleType($rule_type); $cancel_uri = $this->getApplicationURI(); } if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) { $this->requireApplicationCapability( HeraldCapabilityManageGlobalRules::CAPABILITY); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( "This rule was created with a newer version of Herald. You can not ". "view or edit it in this older version. Upgrade your Phabricator ". "deployment."); } // Upgrade rule version to our version, since we might add newly-defined // conditions, etc. $rule->setConfigVersion($local_version); $rule_conditions = $rule->loadConditions(); $rule_actions = $rule->loadActions(); $rule->attachConditions($rule_conditions); $rule->attachActions($rule_actions); $e_name = true; $errors = array(); if ($request->isFormPost() && $request->getStr('save')) { list($e_name, $errors) = $this->saveRule($adapter, $rule, $request); if (!$errors) { $id = $rule->getID(); $uri = $this->getApplicationURI("rule/{$id}/"); return id(new AphrontRedirectResponse())->setURI($uri); } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $must_match_selector = $this->renderMustMatchSelector($rule); $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); $handles = $this->loadHandlesForRule($rule); require_celerity_resource('herald-css'); $content_type_name = $content_type_map[$rule->getContentType()]; $rule_type_name = $rule_type_map[$rule->getRuleType()]; $form = id(new AphrontFormView()) ->setUser($user) ->setID('herald-rule-edit-form') ->addHiddenInput('content_type', $rule->getContentType()) ->addHiddenInput('rule_type', $rule->getRuleType()) ->addHiddenInput('save', 1) ->appendChild( // Build this explicitly (instead of using addHiddenInput()) // so we can add a sigil to it. javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rule', 'sigil' => 'rule', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Rule Name')) ->setName('name') ->setError($e_name) ->setValue($rule->getName())); $form ->appendChild( id(new AphrontFormMarkupControl()) ->setValue(pht( "This %s rule triggers for %s.", phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $content_type_name)))) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Conditions')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-condition', 'mustcapture' => true ), pht('New Condition'))) ->setDescription( pht('When %s these conditions are met:', $must_match_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-conditions', 'class' => 'herald-condition-table' ), ''))) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Action')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-action', 'mustcapture' => true, ), pht('New Action'))) ->setDescription(pht( 'Take these actions %s this rule matches:', $repetition_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-actions', 'class' => 'herald-action-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Rule')) ->addCancelButton($cancel_uri)); $this->setupEditorBehavior($rule, $handles, $adapter); $title = $rule->getID() ? pht('Edit Herald Rule') : pht('Create Herald Rule'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + ->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Edit Rule'), 'device' => true, )); } private function saveRule(HeraldAdapter $adapter, $rule, $request) { $rule->setName($request->getStr('name')); $match_all = ($request->getStr('must_match') == 'all'); $rule->setMustMatchAll((int)$match_all); $repetition_policy_param = $request->getStr('repetition_policy'); $rule->setRepetitionPolicy( HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); $e_name = true; $errors = array(); if (!strlen($rule->getName())) { $e_name = pht("Required"); $errors[] = pht("Rule must have a name."); } $data = json_decode($request->getStr('rule'), true); if (!is_array($data) || !$data['conditions'] || !$data['actions']) { throw new Exception("Failed to decode rule data."); } $conditions = array(); foreach ($data['conditions'] as $condition) { if ($condition === null) { // We manage this as a sparse array on the client, so may receive // NULL if conditions have been removed. continue; } $obj = new HeraldCondition(); $obj->setFieldName($condition[0]); $obj->setFieldCondition($condition[1]); if (is_array($condition[2])) { $obj->setValue(array_keys($condition[2])); } else { $obj->setValue($condition[2]); } try { $adapter->willSaveCondition($obj); } catch (HeraldInvalidConditionException $ex) { $errors[] = $ex->getMessage(); } $conditions[] = $obj; } $actions = array(); foreach ($data['actions'] as $action) { if ($action === null) { // Sparse on the client; removals can give us NULLs. continue; } if (!isset($action[1])) { // Legitimate for any action which doesn't need a target, like // "Do nothing". $action[1] = null; } $obj = new HeraldAction(); $obj->setAction($action[0]); $obj->setTarget($action[1]); try { $adapter->willSaveAction($rule, $obj); } catch (HeraldInvalidActionException $ex) { $errors[] = $ex; } $actions[] = $obj; } $rule->attachConditions($conditions); $rule->attachActions($actions); if (!$errors) { try { $edit_action = $rule->getID() ? 'edit' : 'create'; $rule->openTransaction(); $rule->save(); $rule->saveConditions($conditions); $rule->saveActions($actions); $rule->logEdit($request->getUser()->getPHID(), $edit_action); $rule->saveTransaction(); } catch (AphrontQueryDuplicateKeyException $ex) { $e_name = pht("Not Unique"); $errors[] = pht("Rule name is not unique. Choose a unique name."); } } return array($e_name, $errors); } private function setupEditorBehavior( HeraldRule $rule, array $handles, HeraldAdapter $adapter) { $serial_conditions = array( array('default', 'default', ''), ); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { $value_map = array(); foreach ($value as $k => $fbid) { $value_map[$fbid] = $handles[$fbid]->getName(); } $value = $value_map; } $serial_conditions[] = array( $condition->getFieldName(), $condition->getFieldCondition(), $value, ); } } $serial_actions = array( array('default', ''), ); if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { switch ($action->getAction()) { case HeraldAdapter::ACTION_FLAG: case HeraldAdapter::ACTION_BLOCK: $current_value = $action->getTarget(); break; default: $target_map = array(); foreach ((array)$action->getTarget() as $fbid) { $target_map[$fbid] = $handles[$fbid]->getName(); } $current_value = $target_map; break; } $serial_actions[] = array( $action->getAction(), $current_value, ); } } $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); $all_rules = mpull($all_rules, 'getName', 'getID'); asort($all_rules); $all_fields = $adapter->getFieldNameMap(); $all_conditions = $adapter->getConditionNameMap(); $all_actions = $adapter->getActionNameMap($rule->getRuleType()); $fields = $adapter->getFields(); $field_map = array_select_keys($all_fields, $fields); $actions = $adapter->getActions($rule->getRuleType()); $action_map = array_select_keys($all_actions, $actions); $config_info = array(); $config_info['fields'] = $field_map; $config_info['conditions'] = $all_conditions; $config_info['actions'] = $action_map; foreach ($config_info['fields'] as $field => $name) { $field_conditions = $adapter->getConditionsForField($field); $config_info['conditionMap'][$field] = $field_conditions; } foreach ($config_info['fields'] as $field => $fname) { foreach ($config_info['conditionMap'][$field] as $condition) { $value_type = $adapter->getValueTypeForFieldAndCondition( $field, $condition); $config_info['values'][$field][$condition] = $value_type; } } $config_info['rule_type'] = $rule->getRuleType(); foreach ($config_info['actions'] as $action => $name) { $config_info['targets'][$action] = $adapter->getValueTypeForAction( $action, $rule->getRuleType()); } Javelin::initBehavior( 'herald-rule-editor', array( 'root' => 'herald-rule-edit-form', 'conditions' => (object)$serial_conditions, 'actions' => (object)$serial_actions, 'select' => array( HeraldAdapter::VALUE_CONTENT_SOURCE => array( 'options' => PhabricatorContentSource::getSourceNameMap(), 'default' => PhabricatorContentSource::SOURCE_WEB, ), HeraldAdapter::VALUE_FLAG_COLOR => array( 'options' => PhabricatorFlagColor::getColorNameMap(), 'default' => PhabricatorFlagColor::COLOR_BLUE, ), HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array( 'options' => array( PhabricatorRepositoryPushLog::REFTYPE_BRANCH => pht('branch (git/hg)'), PhabricatorRepositoryPushLog::REFTYPE_TAG => pht('tag (git)'), PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK => pht('bookmark (hg)'), ), 'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH, ), HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array( 'options' => array( PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => pht('change creates ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => pht('change deletes ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => pht('change rewrites ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => pht('dangerous change'), ), 'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD, ), ), 'template' => $this->buildTokenizerTemplates() + array( 'rules' => $all_rules, ), 'author' => array($rule->getAuthorPHID() => $handles[$rule->getAuthorPHID()]->getName()), 'info' => $config_info, )); } private function loadHandlesForRule($rule) { $phids = array(); foreach ($rule->getActions() as $action) { if (!is_array($action->getTarget())) { continue; } foreach ($action->getTarget() as $target) { $target = (array)$target; foreach ($target as $phid) { $phids[] = $phid; } } } foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { foreach ($value as $phid) { $phids[] = $phid; } } } $phids[] = $rule->getAuthorPHID(); return $this->loadViewerHandles($phids); } /** * Render the selector for the "When (all of | any of) these conditions are * met:" element. */ private function renderMustMatchSelector($rule) { return AphrontFormSelectControl::renderSelectTag( $rule->getMustMatchAll() ? 'all' : 'any', array( 'all' => pht('all of'), 'any' => pht('any of'), ), array( 'name' => 'must_match', )); } /** * Render the selector for "Take these actions (every time | only the first * time) this rule matches..." element. */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { $repetition_policy = HeraldRepetitionPolicyConfig::toString( $rule->getRepetitionPolicy()); $repetition_options = $adapter->getRepetitionOptions(); $repetition_names = HeraldRepetitionPolicyConfig::getMap(); $repetition_map = array_select_keys($repetition_names, $repetition_options); if (count($repetition_map) < 2) { return head($repetition_names); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, $repetition_map, array( 'name' => 'repetition_policy', )); } } protected function buildTokenizerTemplates() { $template = new AphrontTokenizerTemplateView(); $template = $template->render(); return array( 'source' => array( 'email' => '/typeahead/common/mailable/', 'user' => '/typeahead/common/accounts/', 'repository' => '/typeahead/common/repositories/', 'package' => '/typeahead/common/packages/', 'project' => '/typeahead/common/projects/', 'userorproject' => '/typeahead/common/accountsorprojects/', 'buildplan' => '/typeahead/common/buildplans/', ), 'markup' => $template, ); } /** * Load rules for the "Another Herald rule..." condition dropdown, which * allows one rule to depend upon the success or failure of another rule. */ private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); // Any rule can depend on a global rule. $all_rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL)) ->withContentTypes(array($rule->getContentType())) ->execute(); if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { // Personal rules may depend upon your other personal rules. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL)) ->withContentTypes(array($rule->getContentType())) ->withAuthorPHIDs(array($rule->getAuthorPHID())) ->execute(); } // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); return $all_rules; } } diff --git a/src/applications/herald/controller/HeraldRuleEditHistoryController.php b/src/applications/herald/controller/HeraldRuleEditHistoryController.php index 03ad026c0..741927228 100644 --- a/src/applications/herald/controller/HeraldRuleEditHistoryController.php +++ b/src/applications/herald/controller/HeraldRuleEditHistoryController.php @@ -1,58 +1,57 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $edit_query = new HeraldEditLogQuery(); if ($this->id) { $edit_query->withRuleIDs(array($this->id)); } $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'offset'); $pager->setOffset($request->getStr('offset')); $edits = $edit_query->executeWithOffsetPager($pager); $need_phids = mpull($edits, 'getEditorPHID'); $handles = $this->loadViewerHandles($need_phids); $list_view = id(new HeraldRuleEditHistoryView()) ->setEdits($edits) ->setHandles($handles) ->setUser($this->getRequest()->getUser()); $panel = new AphrontPanelView(); $panel->setHeader(pht('Edit History')); $panel->appendChild($list_view); $panel->setNoBackground(); $crumbs = $this ->buildApplicationCrumbs($can_create = false) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit History')) - ->setHref($this->getApplicationURI('herald/history'))); + ->addTextCrumb( + pht('Edit History'), + $this->getApplicationURI('herald/history')); $nav = $this->buildSideNavView(); $nav->selectFilter('history'); $nav->appendChild($panel); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Rule Edit History'), 'device' => true, )); } } diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index 7e9105bdf..115f85b71 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -1,186 +1,184 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needConditionsAndActions(true) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($rule->getName()) ->setPolicyObject($rule); if ($rule->getIsDisabled()) { $header->setStatus( 'oh-open', 'red', pht('Disabled')); } else { $header->setStatus( 'oh-open', null, pht('Active')); } $actions = $this->buildActionView($rule); $properties = $this->buildPropertyView($rule, $actions); $id = $rule->getID(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("H{$id}")); + $crumbs->addTextCrumb("H{$id}"); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTimeline($rule); return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, ), array( 'title' => $rule->getName(), 'device' => true, )); } private function buildActionView(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $id = $rule->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($rule) ->setObjectURI($this->getApplicationURI("rule/{$id}/")); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $rule, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Rule')) ->setHref($this->getApplicationURI("edit/{$id}/")) ->setIcon('edit') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($rule->getIsDisabled()) { $disable_uri = "disable/{$id}/enable/"; $disable_icon = 'enable'; $disable_name = pht('Enable Rule'); } else { $disable_uri = "disable/{$id}/disable/"; $disable_icon = 'disable'; $disable_name = pht('Disable Rule'); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Rule')) ->setHref($this->getApplicationURI($disable_uri)) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( HeraldRule $rule, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $this->loadHandles(HeraldAdapter::getHandlePHIDs($rule)); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($rule) ->setActionList($actions); $view->addProperty( pht('Rule Type'), idx(HeraldRuleTypeConfig::getRuleTypeMap(), $rule->getRuleType())); if ($rule->isPersonalRule()) { $view->addProperty( pht('Author'), $this->getHandle($rule->getAuthorPHID())->renderLink()); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { $view->addProperty( pht('Applies To'), idx( HeraldAdapter::getEnabledAdapterMap($viewer), $rule->getContentType())); $view->invokeWillRenderEvent(); $view->addSectionHeader(pht('Rule Description')); $view->addTextContent( phutil_tag( 'div', array( 'style' => 'white-space: pre-wrap;', ), $adapter->renderRuleAsText($rule, $this->getLoadedHandles()))); } return $view; } private function buildTimeline(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $xactions = id(new HeraldTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($rule->getPHID())) ->needComments(true) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); return id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($rule->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); } } diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 22595d3ad..3cbfa47dc 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -1,131 +1,128 @@ getRequest(); $user = $request->getUser(); $request = $this->getRequest(); $object_name = trim($request->getStr('object_name')); $e_name = true; $errors = array(); if ($request->isFormPost()) { if (!$object_name) { $e_name = pht('Required'); $errors[] = pht('An object name is required.'); } if (!$errors) { $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withNames(array($object_name)) ->executeOne(); if (!$object) { $e_name = pht('Invalid'); $errors[] = pht('No object exists with that name.'); } if (!$errors) { // TODO: Let the adapters claim objects instead. if ($object instanceof DifferentialRevision) { $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $object, $object->loadActiveDiff()); } else if ($object instanceof PhabricatorRepositoryCommit) { $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $object->getID()); $adapter = HeraldCommitAdapter::newLegacyAdapter( $object->getRepository(), $object, $data); } else if ($object instanceof ManiphestTask) { $adapter = id(new HeraldManiphestTaskAdapter()) ->setTask($object); } else if ($object instanceof PholioMock) { $adapter = id(new HeraldPholioMockAdapter()) ->setMock($object); } else { throw new Exception("Can not build adapter for object!"); } $rules = id(new HeraldRuleQuery()) ->setViewer($user) ->withContentTypes(array($adapter->getAdapterContentType())) ->withDisabled(false) ->needConditionsAndActions(true) ->needAppliedToPHIDs(array($object->getPHID())) ->needValidateAuthors(true) ->execute(); $engine = id(new HeraldEngine()) ->setDryRun(true); $effects = $engine->applyRules($rules, $adapter); $engine->applyEffects($effects, $adapter, $rules); $xscript = $engine->getTranscript(); return id(new AphrontRedirectResponse()) ->setURI('/herald/transcript/'.$xscript->getID().'/'); } } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $text = pht( 'Enter an object to test rules for, like a Diffusion commit (e.g., '. 'rX123) or a Differential revision (e.g., D123). You will be shown '. 'the results of a dry run on the object.'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $text)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Object Name')) ->setName('object_name') ->setError($e_name) ->setValue($object_name)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Test Rules'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Herald Test Console')) ->setFormError($error_view) ->setForm($form); $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts')) - ->setHref($this->getApplicationURI('/transcript/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Test Console'))); + ->addTextCrumb( + pht('Transcripts'), + $this->getApplicationURI('/transcript/')) + ->addTextCrumb(pht('Test Console')); return $this->buildApplicationPage( $box, array( 'title' => pht('Test Console'), 'device' => true, )); } } diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php index 82413b41e..7e320b235 100644 --- a/src/applications/herald/controller/HeraldTranscriptController.php +++ b/src/applications/herald/controller/HeraldTranscriptController.php @@ -1,517 +1,514 @@ id = $data['id']; $map = $this->getFilterMap(); $this->filter = idx($data, 'filter'); if (empty($map[$this->filter])) { $this->filter = self::FILTER_AFFECTED; } } private function getAdapter() { return $this->adapter; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $xscript = id(new HeraldTranscriptQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$xscript) { return new Aphront404Response(); } require_celerity_resource('herald-test-css'); $nav = $this->buildSideNav(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { $notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Old Transcript')) ->appendChild(phutil_tag( 'p', array(), pht('Details of this transcript have been garbage collected.'))); $nav->appendChild($notice); } else { $map = HeraldAdapter::getEnabledAdapterMap($viewer); $object_type = $object_xscript->getType(); if (empty($map[$object_type])) { // TODO: We should filter these out in the Query, but we have to load // the objectTranscript right now, which is potentially enormous. We // should denormalize the object type, or move the data into a separate // table, and then filter this earlier (and thus raise a better error). // For now, just block access so we don't violate policies. throw new Exception( pht("This transcript has an invalid or inaccessible adapter.")); } $this->adapter = HeraldAdapter::getAdapterForContentType($object_type); $filter = $this->getFilterPHIDs(); $this->filterTranscript($xscript, $filter); $phids = array_merge($filter, $this->getTranscriptPHIDs($xscript)); $phids = array_unique($phids); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $this->handles = $handles; if ($xscript->getDryRun()) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Dry Run')); $notice->appendChild(pht('This was a dry run to test Herald '. 'rules, no actions were executed.')); $nav->appendChild($notice); } $apply_xscript_panel = $this->buildApplyTranscriptPanel( $xscript); $nav->appendChild($apply_xscript_panel); $action_xscript_panel = $this->buildActionTranscriptPanel( $xscript); $nav->appendChild($action_xscript_panel); $object_xscript_panel = $this->buildObjectTranscriptPanel( $xscript); $nav->appendChild($object_xscript_panel); } $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts')) - ->setHref($this->getApplicationURI('/transcript/'))) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($xscript->getID())); + ->addTextCrumb( + pht('Transcripts'), + $this->getApplicationURI('/transcript/')) + ->addTextCrumb($xscript->getID()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Transcript'), 'device' => true, )); } protected function renderConditionTestValue($condition, $handles) { $value = $condition->getTestValue(); if (!is_scalar($value) && $value !== null) { foreach ($value as $key => $phid) { $handle = idx($handles, $phid); if ($handle) { $value[$key] = $handle->getName(); } else { // This shouldn't ever really happen as we are supposed to have // grabbed handles for everything, but be super liberal in what // we accept here since we expect all sorts of weird issues as we // version the system. $value[$key] = 'Unknown Object #'.$phid; } } sort($value); $value = implode(', ', $value); } return phutil_tag('span', array('class' => 'condition-test-value'), $value); } private function buildSideNav() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/herald/transcript/'.$this->id.'/')); $items = array(); $filters = $this->getFilterMap(); foreach ($filters as $key => $name) { $nav->addFilter($key, $name); } $nav->selectFilter($this->filter, null); return $nav; } protected function getFilterMap() { return array( self::FILTER_AFFECTED => pht('Rules that Affected Me'), self::FILTER_OWNED => pht('Rules I Own'), self::FILTER_ALL => pht('All Rules'), ); } protected function getFilterPHIDs() { return array($this->getRequest()->getUser()->getPHID()); } protected function getTranscriptPHIDs($xscript) { $phids = array(); $object_xscript = $xscript->getObjectTranscript(); if (!$object_xscript) { return array(); } $phids[] = $object_xscript->getPHID(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { // TODO: This is total hacks. Add another amazing layer of abstraction. $target = (array)$apply_xscript->getTarget(); foreach ($target as $phid) { if ($phid) { $phids[] = $phid; } } } foreach ($xscript->getRuleTranscripts() as $rule_xscript) { $phids[] = $rule_xscript->getRuleOwner(); } $condition_xscripts = $xscript->getConditionTranscripts(); if ($condition_xscripts) { $condition_xscripts = call_user_func_array( 'array_merge', $condition_xscripts); } foreach ($condition_xscripts as $condition_xscript) { $value = $condition_xscript->getTestValue(); // TODO: Also total hacks. if (is_array($value)) { foreach ($value as $phid) { if ($phid) { // TODO: Probably need to make sure this "looks like" a // PHID or decrease the level of hacks here; this used // to be an is_numeric() check in Facebook land. $phids[] = $phid; } } } } return $phids; } protected function filterTranscript($xscript, $filter_phids) { $filter_owned = ($this->filter == self::FILTER_OWNED); $filter_affected = ($this->filter == self::FILTER_AFFECTED); if (!$filter_owned && !$filter_affected) { // No filtering to be done. return; } if (!$xscript->getObjectTranscript()) { return; } $user_phid = $this->getRequest()->getUser()->getPHID(); $keep_apply_xscripts = array(); $keep_rule_xscripts = array(); $filter_phids = array_fill_keys($filter_phids, true); $rule_xscripts = $xscript->getRuleTranscripts(); foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) { $rule_id = $apply_xscript->getRuleID(); if ($filter_owned) { if (empty($rule_xscripts[$rule_id])) { // No associated rule so you can't own this effect. continue; } if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) { continue; } } else if ($filter_affected) { $targets = (array)$apply_xscript->getTarget(); if (!array_select_keys($filter_phids, $targets)) { continue; } } $keep_apply_xscripts[$id] = true; if ($rule_id) { $keep_rule_xscripts[$rule_id] = true; } } foreach ($rule_xscripts as $rule_id => $rule_xscript) { if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) { $keep_rule_xscripts[$rule_id] = true; } } $xscript->setRuleTranscripts( array_intersect_key( $xscript->getRuleTranscripts(), $keep_rule_xscripts)); $xscript->setApplyTranscripts( array_intersect_key( $xscript->getApplyTranscripts(), $keep_apply_xscripts)); $xscript->setConditionTranscripts( array_intersect_key( $xscript->getConditionTranscripts(), $keep_rule_xscripts)); } private function buildApplyTranscriptPanel($xscript) { $handles = $this->handles; $adapter = $this->getAdapter(); $rule_type_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_names = $adapter->getActionNameMap($rule_type_global); $rows = array(); foreach ($xscript->getApplyTranscripts() as $apply_xscript) { $target = $apply_xscript->getTarget(); switch ($apply_xscript->getAction()) { case HeraldAdapter::ACTION_NOTHING: $target = ''; break; case HeraldAdapter::ACTION_FLAG: $target = PhabricatorFlagColor::getColorName($target); break; case HeraldAdapter::ACTION_BLOCK: // Target is a text string. $target = $target; break; default: if ($target) { foreach ($target as $k => $phid) { $target[$k] = $handles[$phid]->getName(); } $target = implode("\n", $target); } else { $target = ''; } break; } if ($apply_xscript->getApplied()) { $outcome = phutil_tag( 'span', array('class' => 'outcome-success'), pht('SUCCESS')); } else { $outcome = phutil_tag( 'span', array('class' => 'outcome-failure'), pht('FAILURE')); } $rows[] = array( idx($action_names, $apply_xscript->getAction(), pht('Unknown')), $target, hsprintf( 'Taken because: %s
    '. 'Outcome: %s %s', $apply_xscript->getReason(), $outcome, $apply_xscript->getAppliedReason()), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht('No actions were taken.')); $table->setHeaders( array( pht('Action'), pht('Target'), pht('Details'), )); $table->setColumnClasses( array( '', '', 'wide', )); $panel = new AphrontPanelView(); $panel->setHeader(pht('Actions Taken')); $panel->appendChild($table); $panel->setNoBackground(); return $panel; } private function buildActionTranscriptPanel($xscript) { $action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID'); $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $condition_names = $adapter->getConditionNameMap(); $handles = $this->handles; $rule_markup = array(); foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) { $cond_markup = array(); foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) { if ($cond->getNote()) { $note = phutil_tag_div('herald-condition-note', $cond->getNote()); } else { $note = null; } if ($cond->getResult()) { $result = phutil_tag( 'span', array('class' => 'herald-outcome condition-pass'), "\xE2\x9C\x93"); } else { $result = phutil_tag( 'span', array('class' => 'herald-outcome condition-fail'), "\xE2\x9C\x98"); } $cond_markup[] = phutil_tag( 'li', array(), pht( '%s Condition: %s %s %s%s', $result, idx($field_names, $cond->getFieldName(), pht('Unknown')), idx($condition_names, $cond->getCondition(), pht('Unknown')), $this->renderConditionTestValue($cond, $handles), $note)); } if ($rule->getResult()) { $result = phutil_tag( 'span', array('class' => 'herald-outcome rule-pass'), pht('PASS')); $class = 'herald-rule-pass'; } else { $result = phutil_tag( 'span', array('class' => 'herald-outcome rule-fail'), pht('FAIL')); $class = 'herald-rule-fail'; } $cond_markup[] = phutil_tag( 'li', array(), array($result, $rule->getReason())); $user_phid = $this->getRequest()->getUser()->getPHID(); $name = $rule->getRuleName(); $rule_markup[] = phutil_tag( 'li', array( 'class' => $class, ), phutil_tag_div('rule-name', array( phutil_tag('strong', array(), $name), ' ', phutil_tag('ul', array(), $cond_markup), ))); } $panel = ''; if ($rule_markup) { $panel = new AphrontPanelView(); $panel->setHeader(pht('Rule Details')); $panel->setNoBackground(); $panel->appendChild(phutil_tag( 'ul', array('class' => 'herald-explain-list'), $rule_markup)); } return $panel; } private function buildObjectTranscriptPanel($xscript) { $adapter = $this->getAdapter(); $field_names = $adapter->getFieldNameMap(); $object_xscript = $xscript->getObjectTranscript(); $data = array(); if ($object_xscript) { $phid = $object_xscript->getPHID(); $handles = $this->loadViewerHandles(array($phid)); $data += array( pht('Object Name') => $object_xscript->getName(), pht('Object Type') => $object_xscript->getType(), pht('Object PHID') => $phid, pht('Object Link') => $handles[$phid]->renderLink(), ); } $data += $xscript->getMetadataMap(); if ($object_xscript) { foreach ($object_xscript->getFields() as $field => $value) { $field = idx($field_names, $field, '['.$field.'?]'); $data['Field: '.$field] = $value; } } $rows = array(); foreach ($data as $name => $value) { if (!($value instanceof PhutilSafeHTML)) { if (!is_scalar($value) && !is_null($value)) { $value = implode("\n", $value); } if (strlen($value) > 256) { $value = phutil_tag( 'textarea', array( 'class' => 'herald-field-value-transcript', ), $value); } } $rows[] = array($name, $value); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'header', 'wide', )); $panel = new AphrontPanelView(); $panel->setHeader(pht('Object Transcript')); $panel->setNoBackground(); $panel->appendChild($table); return $panel; } } diff --git a/src/applications/herald/controller/HeraldTranscriptListController.php b/src/applications/herald/controller/HeraldTranscriptListController.php index 64335b535..29004e037 100644 --- a/src/applications/herald/controller/HeraldTranscriptListController.php +++ b/src/applications/herald/controller/HeraldTranscriptListController.php @@ -1,88 +1,86 @@ getRequest(); $user = $request->getUser(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $transcripts = id(new HeraldTranscriptQuery()) ->setViewer($user) ->needPartialRecords(true) ->executeWithCursorPager($pager); // Render the table. $handles = array(); if ($transcripts) { $phids = mpull($transcripts, 'getObjectPHID', 'getObjectPHID'); $handles = $this->loadViewerHandles($phids); } $rows = array(); foreach ($transcripts as $xscript) { $rows[] = array( phabricator_date($xscript->getTime(), $user), phabricator_time($xscript->getTime(), $user), $handles[$xscript->getObjectPHID()]->renderLink(), $xscript->getDryRun() ? pht('Yes') : '', number_format((int)(1000 * $xscript->getDuration())).' ms', phutil_tag( 'a', array( 'href' => '/herald/transcript/'.$xscript->getID().'/', 'class' => 'button small grey', ), pht('View Transcript')), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Date'), pht('Time'), pht('Object'), pht('Dry Run'), pht('Duration'), pht('View'), )); $table->setColumnClasses( array( '', 'right', 'wide wrap', '', '', 'action', )); // Render the whole page. $panel = new AphrontPanelView(); $panel->setHeader(pht('Herald Transcripts')); $panel->appendChild($table); $panel->appendChild($pager); $panel->setNoBackground(); $nav = $this->buildSideNavView(); $nav->selectFilter('transcript'); $nav->appendChild($panel); $crumbs = id($this->buildApplicationCrumbs()) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Transcripts'))); + ->addTextCrumb(pht('Transcripts')); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Herald Transcripts'), 'device' => true, )); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php index 1acb6fe4b..ec27cb196 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php @@ -1,198 +1,197 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if (!$this->id) { $is_create = true; $document = id(new LegalpadDocument()) ->setVersions(0) ->setCreatorPHID($user->getPHID()) ->setContributorCount(0) ->setRecentContributorPHIDs(array()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_USER); $body = id(new LegalpadDocumentBody()) ->setCreatorPHID($user->getPHID()); $document->attachDocumentBody($body); $document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID); $title = null; $text = null; } else { $is_create = false; $document = id(new LegalpadDocumentQuery()) ->setViewer($user) ->needDocumentBodies(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$document) { return new Aphront404Response(); } $title = $document->getDocumentBody()->getTitle(); $text = $document->getDocumentBody()->getText(); } $e_title = true; $e_text = true; $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(LegalpadTransactionType::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(LegalpadTransactionType::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 (!$errors) { $editor = id(new LegalpadDocumentEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($user); $xactions = $editor->applyTransactions($document, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('view/'.$document->getID())); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('A Fatal Omission!')) ->setErrors($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($user) ->appendChild( id(new AphrontFormTextControl()) ->setID('document-title') ->setLabel(pht('Title')) ->setError($e_title) ->setValue($title) ->setName('title')) ->appendChild( id(new PhabricatorRemarkupControl()) ->setID('document-text') ->setLabel(pht('Text')) ->setError($e_text) ->setValue($text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('text')); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($document) ->execute(); $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_edit')); $submit = new AphrontFormSubmitControl(); if ($is_create) { $submit->setValue(pht('Create Document')); $title = pht('Create Document'); $short = pht('Create'); } else { $submit->setValue(pht('Update Document')); $submit->addCancelButton( $this->getApplicationURI('view/'.$document->getID())); $title = pht('Update Document'); $short = pht('Update'); } $form ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView())->setName($short)); + $crumbs->addTextCrumb($short); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI($this->getApplicationURI('document/preview/')) ->setControlID('document-text') ->setSkin('document'); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentViewController.php b/src/applications/legalpad/controller/LegalpadDocumentViewController.php index 6ac974e70..379a5f500 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentViewController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentViewController.php @@ -1,217 +1,216 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $document = id(new LegalpadDocumentQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needDocumentBodies(true) ->needContributors(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $xactions = id(new LegalpadTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($document->getPHID())) ->execute(); $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $document_body = $document->getDocumentBody(); $phids = array(); $phids[] = $document_body->getCreatorPHID(); foreach ($subscribers as $subscriber) { $phids[] = $subscriber; } foreach ($document->getContributors() as $contributor) { $phids[] = $contributor; } $this->loadHandles($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $engine->addObject( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $title = $document_body->getTitle(); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($user) ->setPolicyObject($document); $actions = $this->buildActionView($document); $properties = $this->buildPropertyView($document, $engine, $actions); $comment_form_id = celerity_generate_unique_node_id(); $xaction_view = id(new LegalpadTransactionView()) ->setUser($this->getRequest()->getUser()) ->setObjectPHID($document->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment = $this->buildAddCommentView($document, $comment_form_id); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('L'.$document->getID()) - ->setHref($this->getApplicationURI('view/'.$document->getID()))); + $crumbs->addTextCrumb( + 'L'.$document->getID(), + $this->getApplicationURI('view/'.$document->getID())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties) ->addPropertyList($this->buildDocument($engine, $document_body)); $content = array( $crumbs, $object_box, $xaction_view, $add_comment, ); return $this->buildApplicationPage( $content, array( 'title' => $title, 'device' => true, 'pageObjects' => array($document->getPHID()), )); } private function buildDocument( PhabricatorMarkupEngine $engine, LegalpadDocumentBody $body) { $view = new PHUIPropertyListView(); $view->addSectionHeader(pht('Document')); $view->addTextContent( $engine->getOutput($body, LegalpadDocumentBody::MARKUP_FIELD_TEXT)); return $view; } private function buildActionView(LegalpadDocument $document) { $user = $this->getRequest()->getUser(); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($document); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $document, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Document')) ->setHref($this->getApplicationURI('/edit/'.$document->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $actions; } private function buildPropertyView( LegalpadDocument $document, PhabricatorMarkupEngine $engine, PhabricatorActionListView $actions) { $user = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($document) ->setActionList($actions); $properties->addProperty( pht('Last Updated'), phabricator_datetime($document->getDateModified(), $user)); $properties->addProperty( pht('Updated By'), $this->getHandle( $document->getDocumentBody()->getCreatorPHID())->renderLink()); $properties->addProperty( pht('Versions'), $document->getVersions()); $contributor_view = array(); foreach ($document->getContributors() as $contributor) { $contributor_view[] = $this->getHandle($contributor)->renderLink(); } $contributor_view = phutil_implode_html(', ', $contributor_view); $properties->addProperty( pht('Contributors'), $contributor_view); $properties->invokeWillRenderEvent(); return $properties; } private function buildAddCommentView( LegalpadDocument $document, $comment_form_id) { $user = $this->getRequest()->getUser(); $draft = PhabricatorDraft::newFromUserAndKey($user, $document->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('Debate Legislation'); $button_name = $is_serious ? pht('Add Comment') : pht('Commence Filibuster'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($document->getPHID()) ->setFormID($comment_form_id) ->setHeaderText($title) ->setDraft($draft) ->setSubmitButtonName($button_name) ->setAction($this->getApplicationURI('/comment/'.$document->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/macro/controller/PhabricatorMacroAudioController.php b/src/applications/macro/controller/PhabricatorMacroAudioController.php index 8b4b9cfdb..1332163c5 100644 --- a/src/applications/macro/controller/PhabricatorMacroAudioController.php +++ b/src/applications/macro/controller/PhabricatorMacroAudioController.php @@ -1,176 +1,169 @@ id = idx($data, 'id'); } public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroCapabilityManage::CAPABILITY); $request = $this->getRequest(); $viewer = $request->getUser(); $macro = id(new PhabricatorMacroQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) ->withIDs(array($this->id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } $errors = array(); $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); $e_file = null; $file = null; if ($request->isFormPost()) { $xactions = array(); if ($request->getBool('behaviorForm')) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType( PhabricatorMacroTransactionType::TYPE_AUDIO_BEHAVIOR) ->setNewValue($request->getStr('audioBehavior')); } else { $file = null; if ($request->getFileExists('file')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'name' => $request->getStr('name'), 'authorPHID' => $viewer->getPHID(), 'isExplicitUpload' => true, )); } if ($file) { if (!$file->isAudio()) { $errors[] = pht('You must upload audio.'); $e_file = pht('Invalid'); } else { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransactionType::TYPE_AUDIO) ->setNewValue($file->getPHID()); } } else { $errors[] = pht('You must upload an audio file.'); $e_file = pht('Required'); } } if (!$errors) { id(new PhabricatorMacroEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($macro, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $form = id(new AphrontFormView()) ->addHiddenInput('behaviorForm', 1) ->setUser($viewer); $options = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Audio Behavior')) ->setName('audioBehavior') ->setValue( nonempty( $macro->getAudioBehavior(), PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE)); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE, pht('Do Not Play'), pht('Do not play audio.')); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE, pht('Play Once'), pht('Play audio once, when the viewer looks at the macro.')); $options->addButton( PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP, pht('Play Continuously'), pht( 'Play audio continuously, treating the macro as an audio source. '. 'Best for ambient sounds.')); $form->appendChild($options); $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Audio Behavior')) ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); $title = pht('Edit Audio Behavior'); $crumb = pht('Edit Audio'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($view_uri) - ->setName(pht('Macro "%s"', $macro->getName()))); - - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($request->getRequestURI()) - ->setName($crumb)); + $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); + $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') ->setUser($viewer) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('Audio File')) ->setName('file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload File'))); $upload = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Audio')) ->setForm($upload_form); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php index decd17356..b4a2e5ebb 100644 --- a/src/applications/macro/controller/PhabricatorMacroEditController.php +++ b/src/applications/macro/controller/PhabricatorMacroEditController.php @@ -1,280 +1,274 @@ id = idx($data, 'id'); } public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroCapabilityManage::CAPABILITY); $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } } else { $macro = new PhabricatorFileImageMacro(); $macro->setAuthorPHID($user->getPHID()); } $errors = array(); $e_name = true; $e_file = null; $file = null; $can_fetch = PhabricatorEnv::getEnvConfig('security.allow-outbound-http'); if ($request->isFormPost()) { $original = clone $macro; $new_name = null; if ($request->getBool('name_form') || !$macro->getID()) { $new_name = $request->getStr('name'); $macro->setName($new_name); if (!strlen($macro->getName())) { $errors[] = pht('Macro name is required.'); $e_name = pht('Required'); } else if (!preg_match('/^[a-z0-9:_-]{3,}$/', $macro->getName())) { $errors[] = pht( 'Macro must be at least three characters long and contain only '. 'lowercase letters, digits, hyphens, colons and underscores.'); $e_name = pht('Invalid'); } else { $e_name = null; } } $file = null; if ($request->getFileExists('file')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), 'isExplicitUpload' => true, )); } else if ($request->getStr('url')) { try { $file = PhabricatorFile::newFromFileDownload( $request->getStr('url'), array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), 'isExplicitUpload' => true, )); } catch (Exception $ex) { $errors[] = pht('Could not fetch URL: %s', $ex->getMessage()); } } else if ($request->getStr('phid')) { $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($request->getStr('phid'))) ->executeOne(); } if ($file) { if (!$file->isViewableInBrowser()) { $errors[] = pht('You must upload an image.'); $e_file = pht('Invalid'); } else { $macro->setFilePHID($file->getPHID()); $macro->attachFile($file); $e_file = null; } } if (!$macro->getID() && !$file) { $errors[] = pht('You must upload an image to create a macro.'); $e_file = pht('Required'); } if (!$errors) { try { $xactions = array(); if ($new_name !== null) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransactionType::TYPE_NAME) ->setNewValue($new_name); } if ($file) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransactionType::TYPE_FILE) ->setNewValue($file->getPHID()); } $editor = id(new PhabricatorMacroEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($original, $xactions); $view_uri = $this->getApplicationURI('/view/'.$original->getID().'/'); return id(new AphrontRedirectResponse())->setURI($view_uri); } catch (AphrontQueryDuplicateKeyException $ex) { throw $ex; $errors[] = pht('Macro name is not unique!'); $e_name = pht('Duplicate'); } } } if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { $error_view = null; } $current_file = null; if ($macro->getFilePHID()) { $current_file = $macro->getFile(); } $form = new AphrontFormView(); $form->addHiddenInput('name_form', 1); $form->setUser($request->getUser()); $form ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($macro->getName()) ->setCaption( pht('This word or phrase will be replaced with the image.')) ->setError($e_name)); if (!$macro->getID()) { if ($current_file) { $current_file_view = id(new PhabricatorFileLinkView()) ->setFilePHID($current_file->getPHID()) ->setFileName($current_file->getName()) ->setFileViewable(true) ->setFileViewURI($current_file->getBestURI()) ->render(); $form->addHiddenInput('phid', $current_file->getPHID()); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Selected File')) ->setValue($current_file_view)); $other_label = pht('Change File'); } else { $other_label = pht('File'); } if ($can_fetch) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url')) ->setError($request->getFileExists('file') ? false : $e_file)); } $form->appendChild( id(new AphrontFormFileControl()) ->setLabel($other_label) ->setName('file') ->setError($request->getStr('url') ? false : $e_file)); } $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); if ($macro->getID()) { $cancel_uri = $view_uri; } else { $cancel_uri = $this->getApplicationURI(); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Image Macro')) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($macro->getID()) { $title = pht('Edit Image Macro'); $crumb = pht('Edit Macro'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($view_uri) - ->setName(pht('Macro "%s"', $macro->getName()))); + $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); } else { $title = pht('Create Image Macro'); $crumb = pht('Create Macro'); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($request->getRequestURI()) - ->setName($crumb)); + $crumbs->addCrumb($crumb, $request->getRequestURI()); $upload = null; if ($macro->getID()) { $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') ->setUser($request->getUser()); if ($can_fetch) { $upload_form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url'))); } $upload_form ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload File'))); $upload = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New File')) ->setForm($upload_form); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/macro/controller/PhabricatorMacroViewController.php b/src/applications/macro/controller/PhabricatorMacroViewController.php index 472463175..89247cd2b 100644 --- a/src/applications/macro/controller/PhabricatorMacroViewController.php +++ b/src/applications/macro/controller/PhabricatorMacroViewController.php @@ -1,204 +1,203 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } $file = $macro->getFile(); $title_short = pht('Macro "%s"', $macro->getName()); $title_long = pht('Image Macro "%s"', $macro->getName()); $actions = $this->buildActionView($macro); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($this->getApplicationURI('/view/'.$macro->getID().'/')) - ->setName($title_short)); + $crumbs->addTextCrumb( + $title_short, + $this->getApplicationURI('/view/'.$macro->getID().'/')); $properties = $this->buildPropertyView($macro, $actions); if ($file) { $file_view = new PHUIPropertyListView(); $file_view->addImageContent( phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phabricator-image-macro-hero', ))); } $xactions = id(new PhabricatorMacroTransactionQuery()) ->setViewer($request->getUser()) ->withObjectPHIDs(array($macro->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($macro->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $header = id(new PHUIHeaderView()) ->setUser($user) ->setPolicyObject($macro) ->setHeader($title_long); if ($macro->getIsDisabled()) { $header->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setName(pht('Macro Disabled')) ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK)); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $comment_header = $is_serious ? pht('Add Comment') : pht('Grovel in Awe'); $submit_button_name = $is_serious ? pht('Add Comment') : pht('Lavish Praise'); $draft = PhabricatorDraft::newFromUserAndKey($user, $macro->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($macro->getPHID()) ->setDraft($draft) ->setHeaderText($comment_header) ->setAction($this->getApplicationURI('/comment/'.$macro->getID().'/')) ->setSubmitButtonName($submit_button_name); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($file_view) { $object_box->addPropertyList($file_view); } return $this->buildApplicationPage( array( $crumbs, $object_box, $timeline, $add_comment_form, ), array( 'title' => $title_short, 'device' => true, )); } private function buildActionView(PhabricatorFileImageMacro $macro) { $can_manage = $this->hasApplicationCapability( PhabricatorMacroCapabilityManage::CAPABILITY); $request = $this->getRequest(); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($macro) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Macro')) ->setHref($this->getApplicationURI('/edit/'.$macro->getID().'/')) ->setDisabled(!$can_manage) ->setWorkflow(!$can_manage) ->setIcon('edit')); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Audio')) ->setHref($this->getApplicationURI('/audio/'.$macro->getID().'/')) ->setDisabled(!$can_manage) ->setWorkflow(!$can_manage) ->setIcon('herald')); if ($macro->getIsDisabled()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Restore Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) ->setWorkflow(true) ->setDisabled(!$can_manage) ->setIcon('undo')); } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) ->setWorkflow(true) ->setDisabled(!$can_manage) ->setIcon('delete')); } return $view; } private function buildPropertyView( PhabricatorFileImageMacro $macro, PhabricatorActionListView $actions) { $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()) ->setObject($macro) ->setActionList($actions); switch ($macro->getAudioBehavior()) { case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE: $view->addProperty(pht('Audio Behavior'), pht('Play Once')); break; case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP: $view->addProperty(pht('Audio Behavior'), pht('Loop')); break; } $audio_phid = $macro->getAudioPHID(); if ($audio_phid) { $this->loadHandles(array($audio_phid)); $view->addProperty( pht('Audio'), $this->getHandle($audio_phid)->renderLink()); } $view->invokeWillRenderEvent(); return $view; } } diff --git a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php index 09b24383c..aeca4bfea 100644 --- a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php +++ b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php @@ -1,145 +1,141 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->id) { $page_title = pht('Edit Mailing List'); $list = id(new PhabricatorMailingListQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$list) { return new Aphront404Response(); } } else { $page_title = pht('Create Mailing List'); $list = new PhabricatorMetaMTAMailingList(); } $e_email = true; $e_uri = null; $e_name = true; $errors = array(); $crumbs = $this->buildApplicationCrumbs(); if ($request->isFormPost()) { $list->setName($request->getStr('name')); $list->setEmail($request->getStr('email')); $list->setURI($request->getStr('uri')); $e_email = null; $e_name = null; if (!strlen($list->getEmail())) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } if (!strlen($list->getName())) { $e_name = pht('Required'); $errors[] = pht('Name is required.'); } else if (preg_match('/[ ,]/', $list->getName())) { $e_name = pht('Invalid'); $errors[] = pht('Name must not contain spaces or commas.'); } if ($list->getURI()) { if (!PhabricatorEnv::isValidWebResource($list->getURI())) { $e_uri = pht('Invalid'); $errors[] = pht('Mailing list URI must point to a valid web page.'); } } if (!$errors) { try { $list->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI()); } catch (AphrontQueryDuplicateKeyException $ex) { $e_email = pht('Duplicate'); $errors[] = pht('Another mailing list already uses that address.'); } } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($list->getID()) { $form->setAction($this->getApplicationURI('/edit/'.$list->getID().'/')); } else { $form->setAction($this->getApplicationURI('/edit/')); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($list->getEmail()) ->setCaption(pht('Email will be delivered to this address.')) ->setError($e_email)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setError($e_name) ->setCaption(pht('Human-readable display and autocomplete name.')) ->setValue($list->getName())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URI')) ->setName('uri') ->setError($e_uri) ->setCaption(pht('Optional link to mailing list archives or info.')) ->setValue($list->getURI())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($this->getApplicationURI())); if ($list->getID()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Mailing List'))); + $crumbs->addTextCrumb(pht('Edit Mailing List')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Mailing List'))); + $crumbs->addTextCrumb(pht('Create Mailing List')); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $page_title, 'device' => true, )); } } diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index 50e2c87a7..d05870371 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -1,340 +1,338 @@ requireApplicationCapability( ManiphestCapabilityBulkEdit::CAPABILITY); $request = $this->getRequest(); $user = $request->getUser(); $task_ids = $request->getArr('batch'); $tasks = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs($task_ids) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $actions = $request->getStr('actions'); if ($actions) { $actions = json_decode($actions, true); } if ($request->isFormPost() && is_array($actions)) { foreach ($tasks as $task) { $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); $field_list->readFieldsFromStorage($task); $xactions = $this->buildTransactions($actions, $task); if ($xactions) { // TODO: Set content source to "batch edit". $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($task, $xactions); } } $task_ids = implode(',', mpull($tasks, 'getID')); return id(new AphrontRedirectResponse()) ->setURI('/maniphest/?ids='.$task_ids); } $handles = ManiphestTaskListView::loadTaskHandles($user, $tasks); $list = new ManiphestTaskListView(); $list->setTasks($tasks); $list->setUser($user); $list->setHandles($handles); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); require_celerity_resource('maniphest-batch-editor'); Javelin::initBehavior( 'maniphest-batch-editor', array( 'root' => 'maniphest-batch-edit-form', 'tokenizerTemplate' => $template, 'sources' => array( 'project' => array( 'src' => '/typeahead/common/projects/', 'placeholder' => pht('Type a project name...'), ), 'owner' => array( 'src' => '/typeahead/common/searchowner/', 'placeholder' => pht('Type a user name...'), 'limit' => 1, ), 'cc' => array( 'src' => '/typeahead/common/mailable/', 'placeholder' => pht('Type a user name...'), ) ), 'input' => 'batch-form-actions', 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); $form = new AphrontFormView(); $form->setUser($user); $form->setID('maniphest-batch-edit-form'); foreach ($tasks as $task) { $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'batch[]', 'value' => $task->getID(), ))); } $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'actions', 'id' => 'batch-form-actions', ))); $form->appendChild( phutil_tag('p', array(), pht('These tasks will be edited:'))); $form->appendChild($list); $form->appendChild( id(new AphrontFormInsetView()) ->setTitle('Actions') ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'add-action', 'mustcapture' => true, ), pht('Add Another Action'))) ->setContent(javelin_tag( 'table', array( 'sigil' => 'maniphest-batch-actions', 'class' => 'maniphest-batch-actions-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Tasks')) ->addCancelButton('/maniphest/')); $title = pht('Batch Editor'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Batch Edit Tasks')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } private function buildTransactions($actions, ManiphestTask $task) { $value_map = array(); $type_map = array( 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, 'assign' => ManiphestTransaction::TYPE_OWNER, 'status' => ManiphestTransaction::TYPE_STATUS, 'priority' => ManiphestTransaction::TYPE_PRIORITY, 'add_project' => ManiphestTransaction::TYPE_PROJECTS, 'remove_project' => ManiphestTransaction::TYPE_PROJECTS, 'add_ccs' => ManiphestTransaction::TYPE_CCS, 'remove_ccs' => ManiphestTransaction::TYPE_CCS, ); $edge_edit_types = array( 'add_project' => true, 'remove_project' => true, 'add_ccs' => true, 'remove_ccs' => true, ); $xactions = array(); foreach ($actions as $action) { if (empty($type_map[$action['action']])) { throw new Exception("Unknown batch edit action '{$action}'!"); } $type = $type_map[$action['action']]; // Figure out the current value, possibly after modifications by other // batch actions of the same type. For example, if the user chooses to // "Add Comment" twice, we should add both comments. More notably, if the // user chooses "Remove Project..." and also "Add Project...", we should // avoid restoring the removed project in the second transaction. if (array_key_exists($type, $value_map)) { $current = $value_map[$type]; } else { switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $current = null; break; case ManiphestTransaction::TYPE_OWNER: $current = $task->getOwnerPHID(); break; case ManiphestTransaction::TYPE_STATUS: $current = $task->getStatus(); break; case ManiphestTransaction::TYPE_PRIORITY: $current = $task->getPriority(); break; case ManiphestTransaction::TYPE_PROJECTS: $current = $task->getProjectPHIDs(); break; case ManiphestTransaction::TYPE_CCS: $current = $task->getCCPHIDs(); break; } } // Check if the value is meaningful / provided, and normalize it if // necessary. This discards, e.g., empty comments and empty owner // changes. $value = $action['value']; switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (!strlen($value)) { continue 2; } break; case ManiphestTransaction::TYPE_OWNER: if (empty($value)) { continue 2; } $value = head($value); if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $value = null; } break; case ManiphestTransaction::TYPE_PROJECTS: if (empty($value)) { continue 2; } break; case ManiphestTransaction::TYPE_CCS: if (empty($value)) { continue 2; } break; } // If the edit doesn't change anything, go to the next action. This // check is only valid for changes like "owner", "status", etc, not // for edge edits, because we should still apply an edit like // "Remove Projects: A, B" to a task with projects "A, B". if (empty($edge_edit_types[$action['action']])) { if ($value == $current) { continue; } } // Apply the value change; for most edits this is just replacement, but // some need to merge the current and edited values (add/remove project). switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (strlen($current)) { $value = $current."\n\n".$value; } break; case ManiphestTransaction::TYPE_PROJECTS: case ManiphestTransaction::TYPE_CCS: $remove_actions = array( 'remove_project' => true, 'remove_ccs' => true, ); $is_remove = isset($remove_actions[$action['action']]); $current = array_fill_keys($current, true); $value = array_fill_keys($value, true); $new = $current; $did_something = false; if ($is_remove) { foreach ($value as $phid => $ignored) { if (isset($new[$phid])) { unset($new[$phid]); $did_something = true; } } } else { foreach ($value as $phid => $ignored) { if (empty($new[$phid])) { $new[$phid] = true; $did_something = true; } } } if (!$did_something) { continue 2; } $value = array_keys($new); break; } $value_map[$type] = $value; } $template = new ManiphestTransaction(); foreach ($value_map as $type => $value) { $xaction = clone $template; $xaction->setTransactionType($type); switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $xaction->attachComment( id(new ManiphestTransactionComment()) ->setContent($value)); break; default: $xaction->setNewValue($value); break; } $xactions[] = $xaction; } return $xactions; } } diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 67e4b673c..d402b368f 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -1,752 +1,750 @@ view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $uri = $request->getRequestURI(); $project = head($request->getArr('set_project')); $project = nonempty($project, null); $uri = $uri->alter('project', $project); $window = $request->getStr('set_window'); $uri = $uri->alter('window', $window); return id(new AphrontRedirectResponse())->setURI($uri); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/maniphest/report/')); $nav->addLabel(pht('Open Tasks')); $nav->addFilter('user', pht('By User')); $nav->addFilter('project', pht('By Project')); $nav->addLabel(pht('Burnup')); $nav->addFilter('burn', pht('Burnup Rate')); $this->view = $nav->selectFilter($this->view, 'user'); require_celerity_resource('maniphest-report-css'); switch ($this->view) { case 'burn': $core = $this->renderBurn(); break; case 'user': case 'project': $core = $this->renderOpenTasks(); break; default: return new Aphront404Response(); } $nav->appendChild($core); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Reports')))); + ->addTextCrumb(pht('Reports'))); return $this->buildApplicationPage( $nav, array( 'title' => pht('Maniphest Reports'), )); } public function renderBurn() { $request = $this->getRequest(); $user = $request->getUser(); $handle = null; $project_phid = $request->getStr('project'); if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $handle = $handles[$project_phid]; } $table = new ManiphestTransaction(); $conn = $table->establishConnection('r'); $joins = ''; if ($project_phid) { $joins = qsprintf( $conn, 'JOIN %T t ON x.objectPHID = t.phid JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s', id(new ManiphestTask())->getTableName(), id(new ManiphestTaskProject())->getTableName(), $project_phid); } $data = queryfx_all( $conn, 'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q WHERE transactionType = %s ORDER BY x.dateCreated ASC', $table->getTableName(), $joins, ManiphestTransaction::TYPE_STATUS); $stats = array(); $day_buckets = array(); $open_tasks = array(); foreach ($data as $key => $row) { // NOTE: Hack to avoid json_decode(). $oldv = trim($row['oldValue'], '"'); $newv = trim($row['newValue'], '"'); $old_is_open = ($oldv === (string)ManiphestTaskStatus::STATUS_OPEN); $new_is_open = ($newv === (string)ManiphestTaskStatus::STATUS_OPEN); $is_open = ($new_is_open && !$old_is_open); $is_close = ($old_is_open && !$new_is_open); $data[$key]['_is_open'] = $is_open; $data[$key]['_is_close'] = $is_close; if (!$is_open && !$is_close) { // This is either some kind of bogus event, or a resolution change // (e.g., resolved -> invalid). Just skip it. continue; } $day_bucket = phabricator_format_local_time( $row['dateCreated'], $user, 'Yz'); $day_buckets[$day_bucket] = $row['dateCreated']; if (empty($stats[$day_bucket])) { $stats[$day_bucket] = array( 'open' => 0, 'close' => 0, ); } $stats[$day_bucket][$is_close ? 'close' : 'open']++; } $template = array( 'open' => 0, 'close' => 0, ); $rows = array(); $rowc = array(); $last_month = null; $last_month_epoch = null; $last_week = null; $last_week_epoch = null; $week = null; $month = null; $last = last_key($stats) - 1; $period = $template; foreach ($stats as $bucket => $info) { $epoch = $day_buckets[$bucket]; $week_bucket = phabricator_format_local_time( $epoch, $user, 'YW'); if ($week_bucket != $last_week) { if ($week) { $rows[] = $this->formatBurnRow( 'Week of '.phabricator_date($last_week_epoch, $user), $week); $rowc[] = 'week'; } $week = $template; $last_week = $week_bucket; $last_week_epoch = $epoch; } $month_bucket = phabricator_format_local_time( $epoch, $user, 'Ym'); if ($month_bucket != $last_month) { if ($month) { $rows[] = $this->formatBurnRow( phabricator_format_local_time($last_month_epoch, $user, 'F, Y'), $month); $rowc[] = 'month'; } $month = $template; $last_month = $month_bucket; $last_month_epoch = $epoch; } $rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info); $rowc[] = null; $week['open'] += $info['open']; $week['close'] += $info['close']; $month['open'] += $info['open']; $month['close'] += $info['close']; $period['open'] += $info['open']; $period['close'] += $info['close']; } if ($week) { $rows[] = $this->formatBurnRow( pht('Week To Date'), $week); $rowc[] = 'week'; } if ($month) { $rows[] = $this->formatBurnRow( pht('Month To Date'), $month); $rowc[] = 'month'; } $rows[] = $this->formatBurnRow( pht('All Time'), $period); $rowc[] = 'aggregate'; $rows = array_reverse($rows); $rowc = array_reverse($rowc); $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Period'), pht('Opened'), pht('Closed'), pht('Change'), )); $table->setColumnClasses( array( 'right wide', 'n', 'n', 'n', )); if ($handle) { $inst = pht( "NOTE: This table reflects tasks currently in ". "the project. If a task was opened in the past but added to ". "the project recently, it is counted on the day it was ". "opened, not the day it was categorized. If a task was part ". "of this project in the past but no longer is, it is not ". "counted at all."); $header = pht("Task Burn Rate for Project %s", $handle->renderLink()); $caption = phutil_tag('p', array(), $inst); } else { $header = pht("Task Burn Rate for All Tasks"); $caption = null; } $panel = new AphrontPanelView(); $panel->setHeader($header); $panel->setCaption($caption); $panel->appendChild($table); $tokens = array(); if ($handle) { $tokens = array($handle); } $filter = $this->renderReportFilters($tokens, $has_window = false); $id = celerity_generate_unique_node_id(); $chart = phutil_tag( 'div', array( 'id' => $id, 'style' => 'border: 1px solid #6f6f6f; '. 'margin: 1em 2em; '. 'height: 400px; ', ), ''); list($burn_x, $burn_y) = $this->buildSeries($data); require_celerity_resource('raphael-core'); require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array( $burn_x, ), 'y' => array( $burn_y, ), 'xformat' => 'epoch', )); return array($filter, $chart, $panel); } private function renderReportFilters(array $tokens, $has_window) { $request = $this->getRequest(); $user = $request->getUser(); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/searchproject/') ->setLabel(pht('Project')) ->setLimit(1) ->setName('set_project') ->setValue($tokens)); if ($has_window) { list($window_str, $ignored, $window_error) = $this->getWindow(); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Recently Means')) ->setName('set_window') ->setCaption( pht('Configure the cutoff for the "Recently Closed" column.')) ->setValue($window_str) ->setError($window_error)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Filter By Project'))); $filter = new AphrontListFilterView(); $filter->appendChild($form); return $filter; } private function buildSeries(array $data) { $out = array(); $counter = 0; foreach ($data as $row) { $t = (int)$row['dateCreated']; if ($row['_is_close']) { --$counter; $out[$t] = $counter; } else if ($row['_is_open']) { ++$counter; $out[$t] = $counter; } } return array(array_keys($out), array_values($out)); } private function formatBurnRow($label, $info) { $delta = $info['open'] - $info['close']; $fmt = number_format($delta); if ($delta > 0) { $fmt = '+'.$fmt; $fmt = phutil_tag('span', array('class' => 'red'), $fmt); } else { $fmt = phutil_tag('span', array('class' => 'green'), $fmt); } return array( $label, number_format($info['open']), number_format($info['close']), $fmt); } public function renderOpenTasks() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatus(ManiphestTaskQuery::STATUS_OPEN); $project_phid = $request->getStr('project'); $project_handle = null; if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $project_handle = $handles[$project_phid]; $query->withAnyProjects($phids); } $tasks = $query->execute(); $recently_closed = $this->loadRecentlyClosedTasks(); $date = phabricator_date(time(), $user); switch ($this->view) { case 'user': $result = mgroup($tasks, 'getOwnerPHID'); $leftover = idx($result, '', array()); unset($result['']); $result_closed = mgroup($recently_closed, 'getOwnerPHID'); $leftover_closed = idx($result_closed, '', array()); unset($result_closed['']); $base_link = '/maniphest/?assigned='; $leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)')); $col_header = pht('User'); $header = pht('Open Tasks by User and Priority (%s)', $date); break; case 'project': $result = array(); $leftover = array(); foreach ($tasks as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result[$project_phid][] = $task; } } else { $leftover[] = $task; } } $result_closed = array(); $leftover_closed = array(); foreach ($recently_closed as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result_closed[$project_phid][] = $task; } } else { $leftover_closed[] = $task; } } $base_link = '/maniphest/?allProjects[]='; $leftover_name = phutil_tag('em', array(), pht('(No Project)')); $col_header = pht('Project'); $header = pht('Open Tasks by Project and Priority (%s)', $date); break; } $phids = array_keys($result); $handles = $this->loadViewerHandles($phids); $handles = msort($handles, 'getName'); $order = $request->getStr('order', 'name'); list($order, $reverse) = AphrontTableView::parseSort($order); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips', array()); $rows = array(); $pri_total = array(); foreach (array_merge($handles, array(null)) as $handle) { if ($handle) { if (($project_handle) && ($project_handle->getPHID() == $handle->getPHID())) { // If filtering by, e.g., "bugs", don't show a "bugs" group. continue; } $tasks = idx($result, $handle->getPHID(), array()); $name = phutil_tag( 'a', array( 'href' => $base_link.$handle->getPHID(), ), $handle->getName()); $closed = idx($result_closed, $handle->getPHID(), array()); } else { $tasks = $leftover; $name = $leftover_name; $closed = $leftover_closed; } $taskv = $tasks; $tasks = mgroup($tasks, 'getPriority'); $row = array(); $row[] = $name; $total = 0; foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) { $n = count(idx($tasks, $pri, array())); if ($n == 0) { $row[] = '-'; } else { $row[] = number_format($n); } $total += $n; } $row[] = number_format($total); list($link, $oldest_all) = $this->renderOldest($taskv); $row[] = $link; $normal_or_better = array(); foreach ($taskv as $id => $task) { // TODO: This is sort of a hard-code for the default "normal" status. // When reports are more powerful, this should be made more general. if ($task->getPriority() < 50) { continue; } $normal_or_better[$id] = $task; } list($link, $oldest_pri) = $this->renderOldest($normal_or_better); $row[] = $link; if ($closed) { $task_ids = implode(',', mpull($closed, 'getID')); $row[] = phutil_tag( 'a', array( 'href' => '/maniphest/?ids='.$task_ids, 'target' => '_blank', ), number_format(count($closed))); } else { $row[] = '-'; } switch ($order) { case 'total': $row['sort'] = $total; break; case 'oldest-all': $row['sort'] = $oldest_all; break; case 'oldest-pri': $row['sort'] = $oldest_pri; break; case 'closed': $row['sort'] = count($closed); break; case 'name': default: $row['sort'] = $handle ? $handle->getName() : '~'; break; } $rows[] = $row; } $rows = isort($rows, 'sort'); foreach ($rows as $k => $row) { unset($rows[$k]['sort']); } if ($reverse) { $rows = array_reverse($rows); } $cname = array($col_header); $cclass = array('pri right wide'); $pri_map = ManiphestTaskPriority::getShortNameMap(); foreach ($pri_map as $pri => $label) { $cname[] = $label; $cclass[] = 'n'; } $cname[] = 'Total'; $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task.'), 'size' => 200, ), ), pht('Oldest (All)')); $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task, excluding those with Low or '. 'Wishlist priority.'), 'size' => 200, ), ), pht('Oldest (Pri)')); $cclass[] = 'n'; list($ignored, $window_epoch) = $this->getWindow(); $edate = phabricator_datetime($window_epoch, $user); $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Closed after %s', $edate), 'size' => 260 ), ), pht('Recently Closed')); $cclass[] = 'n'; $table = new AphrontTableView($rows); $table->setHeaders($cname); $table->setColumnClasses($cclass); $table->makeSortable( $request->getRequestURI(), 'order', $order, $reverse, array( 'name', null, null, null, null, null, null, 'total', 'oldest-all', 'oldest-pri', 'closed', )); $panel = new AphrontPanelView(); $panel->setHeader($header); $panel->appendChild($table); $tokens = array(); if ($project_handle) { $tokens = array($project_handle); } $filter = $this->renderReportFilters($tokens, $has_window = true); return array($filter, $panel); } /** * Load all the tasks that have been recently closed. */ private function loadRecentlyClosedTasks() { list($ignored, $window_epoch) = $this->getWindow(); $table = new ManiphestTask(); $xtable = new ManiphestTransaction(); $conn_r = $table->establishConnection('r'); $tasks = queryfx_all( $conn_r, 'SELECT t.* FROM %T t JOIN %T x ON x.objectPHID = t.phid WHERE t.status != 0 AND x.oldValue IN (null, %s, %s) AND x.newValue NOT IN (%s, %s) AND t.dateModified >= %d AND x.dateCreated >= %d', $table->getTableName(), $xtable->getTableName(), // TODO: Gross. This table is not meant to be queried like this. Build // real stats tables. json_encode((int)ManiphestTaskStatus::STATUS_OPEN), json_encode((string)ManiphestTaskStatus::STATUS_OPEN), json_encode((int)ManiphestTaskStatus::STATUS_OPEN), json_encode((string)ManiphestTaskStatus::STATUS_OPEN), $window_epoch, $window_epoch); return id(new ManiphestTask())->loadAllFromArray($tasks); } /** * Parse the "Recently Means" filter into: * * - A string representation, like "12 AM 7 days ago" (default); * - a locale-aware epoch representation; and * - a possible error. */ private function getWindow() { $request = $this->getRequest(); $user = $request->getUser(); $window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago'); $error = null; $window_epoch = null; // Do locale-aware parsing so that the user's timezone is assumed for // time windows like "3 PM", rather than assuming the server timezone. $window_epoch = PhabricatorTime::parseLocalTime($window_str, $user); if (!$window_epoch) { $error = 'Invalid'; $window_epoch = time() - (60 * 60 * 24 * 7); } // If the time ends up in the future, convert it to the corresponding time // and equal distance in the past. This is so users can type "6 days" (which // means "6 days from now") and get the behavior of "6 days ago", rather // than no results (because the window epoch is in the future). This might // be a little confusing because it casues "tomorrow" to mean "yesterday" // and "2022" (or whatever) to mean "ten years ago", but these inputs are // nonsense anyway. if ($window_epoch > time()) { $window_epoch = time() - ($window_epoch - time()); } return array($window_str, $window_epoch, $error); } private function renderOldest(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $oldest = null; foreach ($tasks as $id => $task) { if (($oldest === null) || ($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) { $oldest = $id; } } if ($oldest === null) { return array('-', 0); } $oldest = $tasks[$oldest]; $raw_age = (time() - $oldest->getDateCreated()); $age = number_format($raw_age / (24 * 60 * 60)).' d'; $link = javelin_tag( 'a', array( 'href' => '/T'.$oldest->getID(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(), ), 'target' => '_blank', ), $age); return array($link, $raw_age); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index b86d90ce4..4d8d512b0 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,668 +1,665 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $e_title = null; $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } $workflow = $request->getStr('workflow'); $parent_task = null; if ($workflow && is_numeric($workflow)) { $parent_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($workflow)) ->executeOne(); } $transactions = id(new ManiphestTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($task->getPHID())) ->needComments(true) ->execute(); $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); foreach ($field_list->getFields() as $field) { $field->setObject($task); $field->setViewer($user); } $field_list->readFieldsFromStorage($task); $aux_fields = $field_list->getFields(); $e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT; $e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK; $e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK; $e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV; $e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK; $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_commit, $e_dep_on, $e_dep_by, $e_rev, $e_mock, )); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); foreach ($task->getCCPHIDs() as $phid) { $phids[$phid] = true; } foreach ($task->getProjectPHIDs() as $phid) { $phids[$phid] = true; } if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $attached = $task->getAttached(); foreach ($attached as $type => $list) { foreach ($list as $phid => $info) { $phids[$phid] = true; } } if ($parent_task) { $phids[$parent_task->getPHID()] = true; } $phids = array_keys($phids); $this->loadHandles($phids); $handles = $this->getLoadedHandles(); $context_bar = null; if ($parent_task) { $context_bar = new AphrontContextBarView(); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/?parent='.$parent_task->getID(), 'class' => 'green button', ), pht('Create Another Subtask'))); $context_bar->appendChild(hsprintf( 'Created a subtask of %s', $this->getHandle($parent_task->getPHID())->renderLink())); } else if ($workflow == 'create') { $context_bar = new AphrontContextBarView(); $context_bar->addButton(phutil_tag('label', array(), 'Create Another')); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/?template='.$task->getID(), 'class' => 'green button', ), pht('Similar Task'))); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/', 'class' => 'green button', ), pht('Empty Task'))); $context_bar->appendChild(pht('New task created.')); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION); foreach ($transactions as $modern_xaction) { if ($modern_xaction->getComment()) { $engine->addObject( $modern_xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); $transaction_types = array( PhabricatorTransactions::TYPE_COMMENT => pht('Comment'), ManiphestTransaction::TYPE_STATUS => pht('Close Task'), ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'), ManiphestTransaction::TYPE_CCS => pht('Add CCs'), ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'), ManiphestTransaction::TYPE_ATTACH => pht('Upload File'), ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'), ); // Remove actions the user doesn't have permission to take. $requires = array( ManiphestTransaction::TYPE_OWNER => ManiphestCapabilityEditAssign::CAPABILITY, ManiphestTransaction::TYPE_PRIORITY => ManiphestCapabilityEditPriority::CAPABILITY, ManiphestTransaction::TYPE_PROJECTS => ManiphestCapabilityEditProjects::CAPABILITY, ManiphestTransaction::TYPE_STATUS => ManiphestCapabilityEditStatus::CAPABILITY, ); foreach ($transaction_types as $type => $name) { if (isset($requires[$type])) { if (!$this->hasApplicationCapability($requires[$type])) { unset($transaction_types[$type]); } } } if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) { $resolution_types = array_select_keys( $resolution_types, array( ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, ManiphestTaskStatus::STATUS_CLOSED_INVALID, ManiphestTaskStatus::STATUS_CLOSED_SPITE, )); } else { $resolution_types = array( ManiphestTaskStatus::STATUS_OPEN => 'Reopened', ); $transaction_types[ManiphestTransaction::TYPE_STATUS] = 'Reopen Task'; unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]); unset($transaction_types[ManiphestTransaction::TYPE_OWNER]); } $default_claim = array( $user->getPHID() => $user->getUsername().' ('.$user->getRealName().')', ); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if ($draft) { $draft_text = $draft->getDraft(); } else { $draft_text = null; } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { // Prevent tasks from being closed "out of spite" in serious business // installs. unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]); } $comment_form = new AphrontFormView(); $comment_form ->setUser($user) ->setAction('/maniphest/transaction/save/') ->setEncType('multipart/form-data') ->addHiddenInput('taskID', $task->getID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setOptions($transaction_types) ->setID('transaction-action')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Resolution')) ->setName('resolution') ->setControlID('resolution') ->setControlStyle('display: none') ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assign To')) ->setName('assign_to') ->setControlID('assign_to') ->setControlStyle('display: none') ->setID('assign-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CCs')) ->setName('ccs') ->setControlID('ccs') ->setControlStyle('display: none') ->setID('cc-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setControlID('priority') ->setControlStyle('display: none') ->setValue($task->getPriority())) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setControlID('projects') ->setControlStyle('display: none') ->setID('projects-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file') ->setControlID('file') ->setControlStyle('display: none')) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Comments')) ->setName('comments') ->setValue($draft_text) ->setID('transaction-comments') ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($is_serious ? pht('Submit') : pht('Avast!'))); $control_map = array( ManiphestTransaction::TYPE_STATUS => 'resolution', ManiphestTransaction::TYPE_OWNER => 'assign_to', ManiphestTransaction::TYPE_CCS => 'ccs', ManiphestTransaction::TYPE_PRIORITY => 'priority', ManiphestTransaction::TYPE_PROJECTS => 'projects', ManiphestTransaction::TYPE_ATTACH => 'file', ); $tokenizer_map = array( ManiphestTransaction::TYPE_PROJECTS => array( 'id' => 'projects-tokenizer', 'src' => '/typeahead/common/projects/', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a project name...'), ), ManiphestTransaction::TYPE_OWNER => array( 'id' => 'assign-tokenizer', 'src' => '/typeahead/common/users/', 'value' => $default_claim, 'limit' => 1, 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a user name...'), ), ManiphestTransaction::TYPE_CCS => array( 'id' => 'cc-tokenizer', 'src' => '/typeahead/common/mailable/', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a user or mailing list...'), ), ); // TODO: Initializing these behaviors for logged out users fatals things. if ($user->isLoggedIn()) { Javelin::initBehavior('maniphest-transaction-controls', array( 'select' => 'transaction-action', 'controlMap' => $control_map, 'tokenizers' => $tokenizer_map, )); Javelin::initBehavior('maniphest-transaction-preview', array( 'uri' => '/maniphest/transaction/preview/'.$task->getID().'/', 'preview' => 'transaction-preview', 'comments' => 'transaction-comments', 'action' => 'transaction-action', 'map' => $control_map, 'tokenizers' => $tokenizer_map, )); } $comment_header = $is_serious ? pht('Add Comment') : pht('Weigh In'); $preview_panel = phutil_tag_div( 'aphront-panel-preview', phutil_tag( 'div', array('id' => 'transaction-preview'), phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')))); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($task->getPHID()) ->setTransactions($transactions) ->setMarkupEngine($engine); $object_name = 'T'.$task->getID(); $actions = $this->buildActionView($task); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($object_name) - ->setHref('/'.$object_name)) + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($object_name, '/'.$object_name) ->setActionList($actions); $header = $this->buildHeaderView($task); $properties = $this->buildPropertyView( $task, $field_list, $edges, $actions); $description = $this->buildDescriptionView($task, $engine); if (!$user->isLoggedIn()) { // TODO: Eventually, everything should run through this. For now, we're // only using it to get a consistent "Login to Comment" button. $comment_box = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); $preview_panel = null; } else { $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText($comment_header) ->appendChild($comment_form); } $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($description) { $object_box->addPropertyList($description); } return $this->buildApplicationPage( array( $crumbs, $context_bar, $object_box, $timeline, $comment_box, $preview_panel, ), array( 'title' => 'T'.$task->getID().' '.$task->getTitle(), 'pageObjects' => array($task->getPHID()), 'device' => true, )); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription($status); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); return $view; } private function buildActionView(ManiphestTask $task) { $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs()); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($task) ->setObjectURI($this->getRequest()->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('edit') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($task->getOwnerPHID() === $viewer_phid) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Automatically Subscribed')) ->setDisabled(true) ->setIcon('enable')); } else { $action = $viewer_is_cc ? 'rem' : 'add'; $name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'); $icon = $viewer_is_cc ? 'disable' : 'check'; $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setHref("/maniphest/subscribe/{$action}/{$id}/") ->setRenderAsForm(true) ->setUser($viewer) ->setIcon($icon)); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Merge Duplicates In')) ->setHref("/search/attach/{$phid}/TASK/merge/") ->setWorkflow(true) ->setIcon('merge') ->setDisabled(!$can_edit) ->setWorkflow(true)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($this->getApplicationURI("/task/create/?parent={$id}")) ->setIcon('fork')); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Dependencies')) ->setHref("/search/attach/{$phid}/TASK/dependencies/") ->setWorkflow(true) ->setIcon('link') ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($task) ->setActionList($actions); $view->addProperty( pht('Assigned To'), $task->getOwnerPHID() ? $this->getHandle($task->getOwnerPHID())->renderLink() : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Priority'), ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); $view->addProperty( pht('Subscribers'), $task->getCCPHIDs() ? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Author'), $this->getHandle($task->getAuthorPHID())->renderLink()); $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)); } $view->addProperty( pht('Projects'), $task->getProjectPHIDs() ? $this->renderHandlesForPHIDs($task->getProjectPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); $edge_types = array( PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK => pht('Dependent Tasks'), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK => pht('Depends On'), PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV => pht('Differential Revisions'), PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK => pht('Pholio Mocks'), ); $revisions_commits = array(); $handles = $this->getLoadedHandles(); $commit_phids = array_keys( $edges[PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT]); if ($commit_phids) { $commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV; $drev_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($commit_phids) ->withEdgeTypes(array($commit_drev)) ->execute(); foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles[$phid]->renderLink(); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = idx($handles, $revision_phid); if ($revision_handle) { $task_drev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV; unset($edges[$task_drev][$revision_phid]); $revisions_commits[$phid] = hsprintf( '%s / %s', $revision_handle->renderLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if ($edges[$edge_type]) { $view->addProperty( $edge_name, $this->renderHandlesForPHIDs(array_keys($edges[$edge_type]))); } } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $attached = $task->getAttached(); if (!is_array($attached)) { $attached = array(); } $file_infos = idx($attached, PhabricatorFilePHIDTypeFile::TYPECONST); if ($file_infos) { $file_phids = array_keys($file_infos); // TODO: These should probably be handles or something; clean this up // as we sort out file attachments. $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $file_view = new PhabricatorFileLinkListView(); $file_view->setFiles($files); $view->addProperty( pht('Files'), $file_view->render()); } $field_list->appendFieldsToPropertyList( $task, $viewer, $view); $view->invokeWillRenderEvent(); return $view; } private function buildDescriptionView( ManiphestTask $task, PhabricatorMarkupEngine $engine) { $section = null; if (strlen($task->getDescription())) { $section = new PHUIPropertyListView(); $section->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION))); } return $section; } } diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 4038c96a3..0d6e84d72 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -1,692 +1,687 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $can_edit_assign = $this->hasApplicationCapability( ManiphestCapabilityEditAssign::CAPABILITY); $can_edit_policies = $this->hasApplicationCapability( ManiphestCapabilityEditPolicies::CAPABILITY); $can_edit_priority = $this->hasApplicationCapability( ManiphestCapabilityEditPriority::CAPABILITY); $can_edit_projects = $this->hasApplicationCapability( ManiphestCapabilityEditProjects::CAPABILITY); $can_edit_status = $this->hasApplicationCapability( ManiphestCapabilityEditStatus::CAPABILITY); $files = array(); $parent_task = null; $template_id = null; if ($this->id) { $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } } else { $task = ManiphestTask::initializeNewTask($user); // These allow task creation with defaults. if (!$request->isFormPost()) { $task->setTitle($request->getStr('title')); if ($can_edit_projects) { $projects = $request->getStr('projects'); if ($projects) { $tokens = explode(';', $projects); $slug_map = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs($tokens) ->execute(); $name_map = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withNames($tokens) ->execute(); $phid_map = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPHIDs($tokens) ->execute(); $all_map = mpull($slug_map, null, 'getPhrictionSlug') + mpull($name_map, null, 'getName') + mpull($phid_map, null, 'getPHID'); $default_projects = array(); foreach ($tokens as $token) { if (isset($all_map[$token])) { $default_projects[] = $all_map[$token]->getPHID(); } } if ($default_projects) { $task->setProjectPHIDs($default_projects); } } } if ($can_edit_priority) { $priority = $request->getInt('priority'); if ($priority !== null) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if (isset($priority_map[$priority])) { $task->setPriority($priority); } } } $task->setDescription($request->getStr('description')); if ($can_edit_assign) { $assign = $request->getStr('assign'); if (strlen($assign)) { $assign_user = id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withUsernames(array($assign)) ->executeOne(); if (!$assign_user) { $assign_user = id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withPHIDs(array($assign)) ->executeOne(); } if ($assign_user) { $task->setOwnerPHID($assign_user->getPHID()); } } } } $file_phids = $request->getArr('files', array()); if (!$file_phids) { // Allow a single 'file' key instead, mostly since Mac OS X urlencodes // square brackets in URLs when passed to 'open', so you can't 'open' // a URL like '?files[]=xyz' and have PHP interpret it correctly. $phid = $request->getStr('file'); if ($phid) { $file_phids = array($phid); } } if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs($file_phids) ->execute(); } $template_id = $request->getInt('template'); // You can only have a parent task if you're creating a new task. $parent_id = $request->getInt('parent'); if ($parent_id) { $parent_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($parent_id)) ->executeOne(); if (!$template_id) { $template_id = $parent_id; } } } $errors = array(); $e_title = true; $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); foreach ($field_list->getFields() as $field) { $field->setObject($task); $field->setViewer($user); } $field_list->readFieldsFromStorage($task); $aux_fields = $field_list->getFields(); if ($request->isFormPost()) { $changes = array(); $new_title = $request->getStr('title'); $new_desc = $request->getStr('description'); $new_status = $request->getStr('status'); if (!$task->getID()) { $workflow = 'create'; } else { $workflow = ''; } $changes[ManiphestTransaction::TYPE_TITLE] = $new_title; $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc; if ($can_edit_status) { $changes[ManiphestTransaction::TYPE_STATUS] = $new_status; } $owner_tokenizer = $request->getArr('assigned_to'); $owner_phid = reset($owner_tokenizer); if (!strlen($new_title)) { $e_title = pht('Required'); $errors[] = pht('Title is required.'); } $old_values = array(); foreach ($aux_fields as $aux_arr_key => $aux_field) { // TODO: This should be buildFieldTransactionsFromRequest() once we // switch to ApplicationTransactions properly. $aux_old_value = $aux_field->getOldValueForApplicationTransactions(); $aux_field->readValueFromRequest($request); $aux_new_value = $aux_field->getNewValueForApplicationTransactions(); // TODO: We're faking a call to the ApplicaitonTransaction validation // logic here. We need valid objects to pass, but they aren't used // in a meaningful way. For now, build User objects. Once the Maniphest // objects exist, this will switch over automatically. This is a big // hack but shouldn't be long for this world. $placeholder_editor = new PhabricatorUserProfileEditor(); $field_errors = $aux_field->validateApplicationTransactions( $placeholder_editor, PhabricatorTransactions::TYPE_CUSTOMFIELD, array( id(new ManiphestTransaction()) ->setOldValue($aux_old_value) ->setNewValue($aux_new_value), )); foreach ($field_errors as $error) { $errors[] = $error->getMessage(); } $old_values[$aux_field->getFieldKey()] = $aux_old_value; } if ($errors) { $task->setTitle($new_title); $task->setDescription($new_desc); $task->setPriority($request->getInt('priority')); $task->setOwnerPHID($owner_phid); $task->setCCPHIDs($request->getArr('cc')); $task->setProjectPHIDs($request->getArr('projects')); } else { if ($can_edit_priority) { $changes[ManiphestTransaction::TYPE_PRIORITY] = $request->getInt('priority'); } if ($can_edit_assign) { $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; } $changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc'); if ($can_edit_projects) { $changes[ManiphestTransaction::TYPE_PROJECTS] = $request->getArr('projects'); } if ($can_edit_policies) { $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = $request->getStr('viewPolicy'); $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = $request->getStr('editPolicy'); } if ($files) { $file_map = mpull($files, 'getPHID'); $file_map = array_fill_keys($file_map, array()); $changes[ManiphestTransaction::TYPE_ATTACH] = array( PhabricatorFilePHIDTypeFile::TYPECONST => $file_map, ); } $template = new ManiphestTransaction(); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); $transactions[] = $transaction; } if ($aux_fields) { foreach ($aux_fields as $aux_field) { $transaction = clone $template; $transaction->setTransactionType( PhabricatorTransactions::TYPE_CUSTOMFIELD); $aux_key = $aux_field->getFieldKey(); $transaction->setMetadataValue('customfield:key', $aux_key); $old = idx($old_values, $aux_key); $new = $aux_field->getNewValueForApplicationTransactions(); $transaction->setOldValue($old); $transaction->setNewValue($new); $transactions[] = $transaction; } } if ($transactions) { $is_new = !$task->getID(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); } if ($parent_task) { id(new PhabricatorEdgeEditor()) ->setActor($user) ->addEdge( $parent_task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $task->getPHID()) ->save(); $workflow = $parent_task->getID(); } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $this->renderSingleTask($task), )); } $redirect_uri = '/T'.$task->getID(); if ($workflow) { $redirect_uri .= '?workflow='.$workflow; } return id(new AphrontRedirectResponse()) ->setURI($redirect_uri); } } else { if (!$task->getID()) { $task->setCCPHIDs(array( $user->getPHID(), )); if ($template_id) { $template_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($template_id)) ->executeOne(); if ($template_task) { $task->setCCPHIDs($template_task->getCCPHIDs()); $task->setProjectPHIDs($template_task->getProjectPHIDs()); $task->setOwnerPHID($template_task->getOwnerPHID()); $task->setPriority($template_task->getPriority()); $task->setViewPolicy($template_task->getViewPolicy()); $task->setEditPolicy($template_task->getEditPolicy()); $template_fields = PhabricatorCustomField::getObjectFields( $template_task, PhabricatorCustomField::ROLE_EDIT); $fields = $template_fields->getFields(); foreach ($fields as $key => $field) { if (!$field->shouldCopyWhenCreatingSimilarTask()) { unset($fields[$key]); } if (empty($aux_fields[$key])) { unset($fields[$key]); } } if ($fields) { id(new PhabricatorCustomFieldList($fields)) ->readFieldsFromStorage($template_task); foreach ($fields as $key => $field) { $aux_fields[$key]->setValueFromStorage( $field->getValueForStorage()); } } } } } } $phids = array_merge( array($task->getOwnerPHID()), $task->getCCPHIDs(), $task->getProjectPHIDs()); if ($parent_task) { $phids[] = $parent_task->getPHID(); } $phids = array_filter($phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle(pht('Form Errors')); } $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if ($task->getOwnerPHID()) { $assigned_value = array($handles[$task->getOwnerPHID()]); } else { $assigned_value = array(); } if ($task->getCCPHIDs()) { $cc_value = array_select_keys($handles, $task->getCCPHIDs()); } else { $cc_value = array(); } if ($task->getProjectPHIDs()) { $projects_value = array_select_keys($handles, $task->getProjectPHIDs()); } else { $projects_value = array(); } $cancel_id = nonempty($task->getID(), $template_id); if ($cancel_id) { $cancel_uri = '/T'.$cancel_id; } else { $cancel_uri = '/maniphest/'; } if ($task->getID()) { $button_name = pht('Save Task'); $header_name = pht('Edit Task'); } else if ($parent_task) { $cancel_uri = '/T'.$parent_task->getID(); $button_name = pht('Create Task'); $header_name = pht('Create New Subtask'); } else { $button_name = pht('Create Task'); $header_name = pht('Create New Task'); } require_celerity_resource('maniphest-task-edit-css'); $project_tokenizer_id = celerity_generate_unique_node_id(); if ($request->isAjax()) { $form = new PHUIFormLayoutView(); } else { $form = new AphrontFormView(); $form ->setUser($user) ->addHiddenInput('template', $template_id); } if ($parent_task) { $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Parent Task')) ->setValue($handles[$parent_task->getPHID()]->getFullName())) ->addHiddenInput('parent', $parent_task->getID()); } $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Title')) ->setName('title') ->setError($e_title) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($task->getTitle())); if ($task->getID() && $can_edit_status) { // Only show this in "edit" mode, not "create" mode, since creating a // non-open task is kind of silly and it would just clutter up the // "create" interface. $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setValue($task->getStatus()) ->setOptions(ManiphestTaskStatus::getTaskStatusMap())); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($task) ->execute(); if ($can_edit_assign) { $form->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assigned To')) ->setName('assigned_to') ->setValue($assigned_value) ->setUser($user) ->setDatasource('/typeahead/common/users/') ->setLimit(1)); } $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CC')) ->setName('cc') ->setValue($cc_value) ->setUser($user) ->setDatasource('/typeahead/common/mailable/')); if ($can_edit_priority) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setValue($task->getPriority())); } if ($can_edit_policies) { $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($task) ->setPolicies($policies) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($task) ->setPolicies($policies) ->setName('editPolicy')); } if ($can_edit_projects) { $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($projects_value) ->setID($project_tokenizer_id) ->setCaption( javelin_tag( 'a', array( 'href' => '/project/create/', 'mustcapture' => true, 'sigil' => 'project-create', ), pht('Create New Project'))) ->setDatasource('/typeahead/common/projects/')); } foreach ($aux_fields as $aux_field) { $aux_control = $aux_field->renderEditControl(); $form->appendChild($aux_control); } require_celerity_resource('aphront-error-view-css'); Javelin::initBehavior('project-create', array( 'tokenizerID' => $project_tokenizer_id, )); if ($files) { $file_display = mpull($files, 'getName'); $file_display = phutil_implode_html(phutil_tag('br'), $file_display); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Files')) ->setValue($file_display)); foreach ($files as $ii => $file) { $form->addHiddenInput('files['.$ii.']', $file->getPHID()); } } $description_control = new PhabricatorRemarkupControl(); // "Upsell" creating tasks via email in create flows if the instance is // configured for this awesomeness. $email_create = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if (!$task->getID() && $email_create) { $email_hint = pht( 'You can also create tasks by sending an email to: %s', phutil_tag('tt', array(), $email_create)); $description_control->setCaption($email_hint); } $description_control ->setLabel(pht('Description')) ->setName('description') ->setID('description-textarea') ->setValue($task->getDescription()) ->setUser($user); $form ->appendChild($description_control); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($header_name) ->appendChild( array( $error_view, $form, )) ->addCancelButton($cancel_uri) ->addSubmitButton($button_name); return id(new AphrontDialogResponse())->setDialog($dialog); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button_name)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($header_name) ->setFormError($error_view) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Description Preview')) ->setControlID('description-textarea') ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/')); if ($task->getID()) { $page_objects = array($task->getPHID()); } else { $page_objects = array(); } $crumbs = $this->buildApplicationCrumbs(); if ($task->getID()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('T'.$task->getID()) - ->setHref('/T'.$task->getID())); + $crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID()); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($header_name)); + $crumbs->addTextCrumb($header_name); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => $header_name, 'pageObjects' => $page_objects, 'device' => true, )); } } diff --git a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php index 0b756f853..ba85f5e9d 100644 --- a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php +++ b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php @@ -1,155 +1,153 @@ application = $data['application']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $selected = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withClasses(array($this->application)) ->executeOne(); if (!$selected) { return new Aphront404Response(); } $title = $selected->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($selected->getName())); + $crumbs->addTextCrumb($selected->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($user) ->setPolicyObject($selected); if ($selected->isInstalled()) { $header->setStatus('oh-ok', null, pht('Installed')); } else { $header->setStatus('policy-noone', null, pht('Uninstalled')); } $actions = $this->buildActionView($user, $selected); $properties = $this->buildPropertyView($selected, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } private function buildPropertyView( PhabricatorApplication $application, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->addProperty(pht('Description'), $application->getShortDescription()); $properties->setActionList($actions); if ($application->isBeta()) { $properties->addProperty( pht('Release'), pht('Beta')); } $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $application); $properties->addSectionHeader(pht('Policies')); foreach ($application->getCapabilities() as $capability) { $properties->addProperty( $application->getCapabilityLabel($capability), idx($descriptions, $capability)); } return $properties; } private function buildActionView( PhabricatorUser $user, PhabricatorApplication $selected) { $view = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $selected, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = $this->getApplicationURI('edit/'.get_class($selected).'/'); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Policies')) ->setIcon('edit') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($edit_uri)); if ($selected->canUninstall()) { if ($selected->isInstalled()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Uninstall')) ->setIcon('delete') ->setDisabled(!$can_edit) ->setWorkflow(true) ->setHref( $this->getApplicationURI(get_class($selected).'/uninstall/'))); } else { $action = id(new PhabricatorActionView()) ->setName(pht('Install')) ->setIcon('new') ->setDisabled(!$can_edit) ->setWorkflow(true) ->setHref( $this->getApplicationURI(get_class($selected).'/install/')); $beta_enabled = PhabricatorEnv::getEnvConfig( 'phabricator.show-beta-applications'); if ($selected->isBeta() && !$beta_enabled) { $action->setDisabled(true); } $view->addAction($action); } } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Uninstall')) ->setIcon('delete') ->setWorkflow(true) ->setDisabled(true) ->setHref( $this->getApplicationURI(get_class($selected).'/uninstall/'))); } return $view; } } diff --git a/src/applications/meta/controller/PhabricatorApplicationEditController.php b/src/applications/meta/controller/PhabricatorApplicationEditController.php index f18663ce2..aa4952205 100644 --- a/src/applications/meta/controller/PhabricatorApplicationEditController.php +++ b/src/applications/meta/controller/PhabricatorApplicationEditController.php @@ -1,175 +1,170 @@ application = $data['application']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $application = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withClasses(array($this->application)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$application) { return new Aphront404Response(); } $title = $application->getName(); $view_uri = $this->getApplicationURI('view/'.get_class($application).'/'); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($application) ->execute(); if ($request->isFormPost()) { $result = array(); foreach ($application->getCapabilities() as $capability) { $old = $application->getPolicy($capability); $new = $request->getStr('policy:'.$capability); if ($old == $new) { // No change to the setting. continue; } if (empty($policies[$new])) { // Not a standard policy, check for a custom policy. $policy = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->withPHIDs(array($new)) ->executeOne(); if (!$policy) { // Not a custom policy either. Can't set the policy to something // invalid, so skip this. continue; } } if ($new == PhabricatorPolicies::POLICY_PUBLIC) { $capobj = PhabricatorPolicyCapability::getCapabilityByKey( $capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { // Can't set non-public policies to public. continue; } } $result[$capability] = $new; } if ($result) { $key = 'phabricator.application-settings'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $value = $config_entry->getValue(); $phid = $application->getPHID(); if (empty($value[$phid])) { $value[$application->getPHID()] = array(); } if (empty($value[$phid]['policy'])) { $value[$phid]['policy'] = array(); } $value[$phid]['policy'] = $result + $value[$phid]['policy']; // Don't allow users to make policy edits which would lock them out of // applications, since they would be unable to undo those actions. PhabricatorEnv::overrideConfig($key, $value); PhabricatorPolicyFilter::mustRetainCapability( $user, $application, PhabricatorPolicyCapability::CAN_VIEW); PhabricatorPolicyFilter::mustRetainCapability( $user, $application, PhabricatorPolicyCapability::CAN_EDIT); PhabricatorConfigEditor::storeNewValue( $config_entry, $value, $this->getRequest()); } return id(new AphrontRedirectResponse())->setURI($view_uri); } $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $application); $form = id(new AphrontFormView()) ->setUser($user); foreach ($application->getCapabilities() as $capability) { $label = $application->getCapabilityLabel($capability); $can_edit = $application->isCapabilityEditable($capability); $caption = $application->getCapabilityCaption($capability); if (!$can_edit) { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel($label) ->setValue(idx($descriptions, $capability)) ->setCaption($caption)); } else { $form->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability($capability) ->setPolicyObject($application) ->setPolicies($policies) ->setLabel($label) ->setName('policy:'.$capability) ->setCaption($caption)); } } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Policies')) ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($application->getName()) - ->setHref($view_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Policies'))); + $crumbs->addTextCrumb($application->getName(), $view_uri); + $crumbs->addTextCrumb(pht('Edit Policies')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Policies: %s', $application->getName())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/nuance/controller/NuanceSourceViewController.php b/src/applications/nuance/controller/NuanceSourceViewController.php index 2f2c4037d..e0031d1d8 100644 --- a/src/applications/nuance/controller/NuanceSourceViewController.php +++ b/src/applications/nuance/controller/NuanceSourceViewController.php @@ -1,136 +1,134 @@ sourceID = $source_id; return $this; } public function getSourceID() { return $this->sourceID; } public function willProcessRequest(array $data) { $this->setSourceID($data['id']); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $source_id = $this->getSourceID(); $source = id(new NuanceSourceQuery()) ->setViewer($viewer) ->withIDs(array($source_id)) ->executeOne(); if (!$source) { return new Aphront404Response(); } $source_phid = $source->getPHID(); $xactions = id(new NuanceSourceTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($source_phid)) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($source_phid) ->setMarkupEngine($engine) ->setTransactions($xactions); $title = pht('%s', $source->getName()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $header = $this->buildHeaderView($source); $actions = $this->buildActionView($source); $properties = $this->buildPropertyView($source, $actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, ), array( 'title' => $title, 'device' => true, )); } private function buildHeaderView(NuanceSource $source) { $viewer = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($source->getName()) ->setPolicyObject($source); return $header; } private function buildActionView(NuanceSource $source) { $viewer = $this->getRequest()->getUser(); $id = $source->getID(); $actions = id(new PhabricatorActionListView()) ->setObjectURI($source->getURI()) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $source, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Source')) ->setIcon('edit') ->setHref($this->getApplicationURI("source/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $actions; } private function buildPropertyView( NuanceSource $source, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($source) ->setActionList($actions); $definition = NuanceSourceDefinition::getDefinitionForSource($source); $properties->addProperty( pht('Source Type'), $definition->getName()); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $source); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); return $properties; } } diff --git a/src/applications/owners/controller/PhabricatorOwnersController.php b/src/applications/owners/controller/PhabricatorOwnersController.php index a68060c9f..ff74fb14f 100644 --- a/src/applications/owners/controller/PhabricatorOwnersController.php +++ b/src/applications/owners/controller/PhabricatorOwnersController.php @@ -1,71 +1,69 @@ filter; } protected function setSideNavFilter($filter) { $this->filter = $filter; return $this; } public function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $base_uri = new PhutilURI('/owners/'); $nav->setBaseURI($base_uri); $nav->addLabel(pht('Packages')); $this->getExtraPackageViews($nav); $nav->addFilter('view/owned', pht('Owned')); $nav->addFilter('view/projects', pht('Projects')); $nav->addFilter('view/all', pht('All')); $nav->selectFilter($this->getSideNavFilter(), 'view/owned'); $filter = $nav->getSelectedFilter(); switch ($filter) { case 'view/owned': $title = pht('Owned Packages'); break; case 'view/all': $title = pht('All Packages'); break; case 'view/projects': $title = pht('Projects'); break; case 'new': $title = pht('New Package'); break; default: $title = pht('Package'); break; } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Package')) ->setHref('/owners/new/') ->setIcon('create')); $nav->setCrumbs($crumbs); return $nav; } public function buildApplicationMenu() { return $this->buildSideNavView()->getMenu(); } protected function getExtraPackageViews(AphrontSideNavFilterView $view) { return; } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php index 0ae3da1e7..d311dc9d0 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php @@ -1,78 +1,76 @@ getRequest(); $viewer = $request->getUser(); $types = PassphraseCredentialType::getAllTypes(); $types = mpull($types, null, 'getCredentialType'); $types = msort($types, 'getCredentialTypeName'); $errors = array(); $e_type = null; if ($request->isFormPost()) { $type = $request->getStr('type'); if (empty($types[$type])) { $errors[] = pht('You must choose a credential type.'); $e_type = pht('Required'); } if (!$errors) { $uri = $this->getApplicationURI('edit/?type='.$type); return id(new AphrontRedirectResponse())->setURI($uri); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setErrors($errors); } $types_control = id(new AphrontFormRadioButtonControl()) ->setName('type') ->setLabel(pht('Credential Type')) ->setError($e_type); foreach ($types as $type) { $types_control->addButton( $type->getCredentialType(), $type->getCredentialTypeName(), $type->getCredentialTypeDescription()); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($types_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue')) ->addCancelButton($this->getApplicationURI())); $title = pht('New Credential'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create'))); + $crumbs->addTextCrumb(pht('Create')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Credential')) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index f48b45e65..8846db5a2 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -1,270 +1,265 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->id) { $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } $is_new = false; } else { $type_const = $request->getStr('type'); $type = PassphraseCredentialType::getTypeByConstant($type_const); if (!$type) { return new Aphront404Response(); } $credential = PassphraseCredential::initializeNewCredential($viewer) ->setCredentialType($type->getCredentialType()) ->setProvidesType($type->getProvidesType()); $is_new = true; // Prefill username if provided. $credential->setUsername($request->getStr('username')); } $errors = array(); $v_name = $credential->getName(); $e_name = true; $v_desc = $credential->getDescription(); $v_username = $credential->getUsername(); $e_username = true; $bullet = "\xE2\x80\xA2"; $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_username = $request->getStr('username'); $v_secret = $request->getStr('secret'); $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); $type_name = PassphraseCredentialTransaction::TYPE_NAME; $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; $xactions = array(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_desc) ->setNewValue($v_desc); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_username) ->setNewValue($v_username); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_view_policy) ->setNewValue($v_view_policy); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_edit_policy) ->setNewValue($v_edit_policy); // Open a transaction in case we're writing a new secret; this limits // the amount of code which handles secret plaintexts. $credential->openTransaction(); $min_secret = str_replace($bullet, '', trim($v_secret)); if (strlen($min_secret)) { // If the credential was previously destroyed, restore it when it is // edited if a secret is provided. $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_destroy) ->setNewValue(0); $new_secret = id(new PassphraseSecret()) ->setSecretData($v_secret) ->save(); $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret_id) ->setNewValue($new_secret->getID()); } try { $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); $credential->saveTransaction(); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent( array( 'phid' => $credential->getPHID(), 'name' => 'K'.$credential->getID().' '.$credential->getName(), )); } else { return id(new AphrontRedirectResponse()) ->setURI('/K'.$credential->getID()); } } catch (PhabricatorApplicationTransactionValidationException $ex) { $credential->killTransaction(); $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); $e_username = $ex->getShortMessage($type_username); $credential->setViewPolicy($v_view_policy); $credential->setEditPolicy($v_edit_policy); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($credential) ->execute(); $secret_control = $type->newSecretControl(); if ($request->isAjax()) { $form = new PHUIFormLayoutView(); } else { $form = id(new AphrontFormView()) ->setUser($viewer); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Credential Type')) ->setValue($type->getCredentialTypeName())) ->appendChild( id(new AphrontFormDividerControl())) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($credential) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($credential) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormDividerControl())) ->appendChild( id(new AphrontFormTextControl()) ->setName('username') ->setLabel(pht('Login/Username')) ->setValue($v_username) ->setError($e_username)) ->appendChild( $secret_control ->setName('secret') ->setLabel($type->getSecretLabel()) ->setValue($v_secret)); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $title = pht('Create Credential'); $header = pht('Create New Credential'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create'))); + $crumbs->addTextCrumb(pht('Create')); } else { $title = pht('Edit Credential'); $header = pht('Edit Credential %s', 'K'.$credential->getID()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('K'.$credential->getID()) - ->setHref('/K'.$credential->getID())); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb( + 'K'.$credential->getID(), + '/K'.$credential->getID()); + $crumbs->addTextCrumb(pht('Edit')); } if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) ->appendChild($form) ->addSubmitButton(pht('Create Credential')) ->addCancelButton($this->getApplicationURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($this->getApplicationURI())); $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setValidationException($validation_exception) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index 9499bbc55..5ff0181bd 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -1,181 +1,179 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $type = PassphraseCredentialType::getTypeByConstant( $credential->getCredentialType()); if (!$type) { throw new Exception(pht('Credential has invalid type "%s"!', $type)); } $xactions = id(new PassphraseCredentialTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($credential->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($credential->getPHID()) ->setTransactions($xactions); $title = pht('%s %s', 'K'.$credential->getID(), $credential->getName()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('K'.$credential->getID())); + $crumbs->addTextCrumb('K'.$credential->getID()); $header = $this->buildHeaderView($credential); $actions = $this->buildActionView($credential); $properties = $this->buildPropertyView($credential, $type, $actions); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $box, $timeline, ), array( 'title' => $title, 'device' => true, )); } private function buildHeaderView(PassphraseCredential $credential) { $viewer = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($credential->getName()) ->setPolicyObject($credential); if ($credential->getIsDestroyed()) { $header->setStatus('reject', 'red', pht('Destroyed')); } return $header; } private function buildActionView(PassphraseCredential $credential) { $viewer = $this->getRequest()->getUser(); $id = $credential->getID(); $actions = id(new PhabricatorActionListView()) ->setObjectURI('/K'.$id) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $credential, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Credential')) ->setIcon('edit') ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if (!$credential->getIsDestroyed()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Destroy Credential')) ->setIcon('delete') ->setHref($this->getApplicationURI("destroy/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Secret')) ->setIcon('preview') ->setHref($this->getApplicationURI("reveal/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } return $actions; } private function buildPropertyView( PassphraseCredential $credential, PassphraseCredentialType $type, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($credential) ->setActionList($actions); $properties->addProperty( pht('Credential Type'), $type->getCredentialTypeName()); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $credential); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $properties->addProperty( pht('Username'), $credential->getUsername()); $used_by_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $credential->getPHID(), PhabricatorEdgeConfig::TYPE_CREDENTIAL_USED_BY_OBJECT); if ($used_by_phids) { $this->loadHandles($used_by_phids); $properties->addProperty( pht('Used By'), $this->renderHandlesForPHIDs($used_by_phids)); } $description = $credential->getDescription(); if (strlen($description)) { $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent($description), 'default', $viewer)); } return $properties; } } diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php index 185b4b033..e83cf6656 100644 --- a/src/applications/paste/controller/PhabricatorPasteEditController.php +++ b/src/applications/paste/controller/PhabricatorPasteEditController.php @@ -1,234 +1,230 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $parent = null; $parent_id = null; if (!$this->id) { $is_create = true; $paste = PhabricatorPaste::initializeNewPaste($user); $parent_id = $request->getStr('parent'); if ($parent_id) { // NOTE: If the Paste is forked from a paste which the user no longer // has permission to see, we still let them edit it. $parent = id(new PhabricatorPasteQuery()) ->setViewer($user) ->withIDs(array($parent_id)) ->needContent(true) ->needRawContent(true) ->execute(); $parent = head($parent); if ($parent) { $paste->setParentPHID($parent->getPHID()); $paste->setViewPolicy($parent->getViewPolicy()); } } $paste->setAuthorPHID($user->getPHID()); } else { $is_create = false; $paste = id(new PhabricatorPasteQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$paste) { return new Aphront404Response(); } } $text = null; $e_text = true; $errors = array(); if ($is_create && $parent) { $v_title = pht('Fork of %s', $parent->getFullName()); $v_language = $parent->getLanguage(); $v_text = $parent->getRawContent(); } else { $v_title = $paste->getTitle(); $v_language = $paste->getLanguage(); $v_text = ''; } $v_policy = $paste->getViewPolicy(); if ($request->isFormPost()) { $xactions = array(); if ($is_create) { $v_text = $request->getStr('text'); if (!strlen($v_text)) { $e_text = pht('Required'); $errors[] = pht('The paste may not be blank.'); } else { $e_text = null; } } $v_title = $request->getStr('title'); $v_language = $request->getStr('language'); $v_policy = $request->getStr('can_view'); // NOTE: The author is the only editor and can always view the paste, // so it's impossible for them to choose an invalid policy. if (!$errors) { if ($is_create) { $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CREATE) ->setNewValue(array( 'title' => $v_title, 'text' => $v_text)); } $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) ->setNewValue($v_title); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) ->setNewValue($v_language); $xactions[] = id(new PhabricatorPasteTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_policy); $editor = id(new PhabricatorPasteEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $xactions = $editor->applyTransactions($paste, $xactions); return id(new AphrontRedirectResponse())->setURI($paste->getURI()); } else { // make sure we update policy so its correctly populated to what // the user chose $paste->setViewPolicy($v_policy); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('A Fatal Omission!')) ->setErrors($errors); } $form = new AphrontFormView(); $langs = array( '' => pht('(Detect From Filename in Title)'), ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); $form ->setUser($user) ->addHiddenInput('parent', $parent_id) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($v_title) ->setName('title')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Language')) ->setName('language') ->setValue($v_language) ->setOptions($langs)); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($paste) ->execute(); $form->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($paste) ->setPolicies($policies) ->setName('can_view')); if ($is_create) { $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Text')) ->setError($e_text) ->setValue($v_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setCustomClass('PhabricatorMonospaced') ->setName('text')); } else { $fork_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('?parent='.$paste->getID()) ), pht('Fork')); $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Text')) ->setValue(pht( 'Paste text can not be edited. %s to create a new paste.', $fork_link))); } $submit = new AphrontFormSubmitControl(); if (!$is_create) { $submit->addCancelButton($paste->getURI()); $submit->setValue(pht('Save Paste')); $title = pht('Edit %s', $paste->getFullName()); $short = pht('Edit'); } else { $submit->setValue(pht('Create Paste')); $title = pht('Create New Paste'); $short = pht('Create'); } $form->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if (!$is_create) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('P'.$paste->getID()) - ->setHref('/P'.$paste->getID())); + $crumbs->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView())->setName($short)); + $crumbs->addTextCrumb($short); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index 1e10825f8..be756de59 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -1,240 +1,237 @@ id = $data['id']; $raw_lines = idx($data, 'lines'); $map = array(); if ($raw_lines) { $lines = explode('-', $raw_lines); $first = idx($lines, 0, 0); $last = idx($lines, 1); if ($last) { $min = min($first, $last); $max = max($first, $last); $map = array_fuse(range($min, $max)); } else { $map[$first] = $first; } } $this->highlightMap = $map; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $paste = id(new PhabricatorPasteQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needContent(true) ->executeOne(); if (!$paste) { return new Aphront404Response(); } $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($paste->getFilePHID())) ->executeOne(); if (!$file) { return new Aphront400Response(); } $forks = id(new PhabricatorPasteQuery()) ->setViewer($user) ->withParentPHIDs(array($paste->getPHID())) ->execute(); $fork_phids = mpull($forks, 'getPHID'); $this->loadHandles( array_merge( array( $paste->getAuthorPHID(), $paste->getParentPHID(), ), $fork_phids)); $header = $this->buildHeaderView($paste); $actions = $this->buildActionView($user, $paste, $file); $properties = $this->buildPropertyView($paste, $fork_phids, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $source_code = $this->buildSourceCodeView( $paste, null, $this->highlightMap); $source_code = id(new PHUIBoxView()) ->appendChild($source_code) ->setBorder(true) ->addMargin(PHUI::MARGIN_LARGE_LEFT) ->addMargin(PHUI::MARGIN_LARGE_RIGHT) ->addMargin(PHUI::MARGIN_LARGE_TOP); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) ->setActionList($actions) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('P'.$paste->getID()) - ->setHref('/P'.$paste->getID())); + ->addTextCrumb('P'.$paste->getID(), '/P'.$paste->getID()); $xactions = id(new PhabricatorPasteTransactionQuery()) ->setViewer($request->getUser()) ->withObjectPHIDs(array($paste->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($paste->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Debate Paste Accuracy'); $submit_button_name = $is_serious ? pht('Add Comment') : pht('Pity the Fool'); $draft = PhabricatorDraft::newFromUserAndKey($user, $paste->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($paste->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$paste->getID().'/')) ->setSubmitButtonName($submit_button_name); return $this->buildApplicationPage( array( $crumbs, $object_box, $source_code, $timeline, $add_comment_form, ), array( 'title' => $paste->getFullName(), 'device' => true, 'pageObjects' => array($paste->getPHID()), )); } private function buildHeaderView(PhabricatorPaste $paste) { $title = (nonempty($paste->getTitle())) ? $paste->getTitle() : pht('(An Untitled Masterwork)'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($paste); return $header; } private function buildActionView( PhabricatorUser $user, PhabricatorPaste $paste, PhabricatorFile $file) { $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $paste, PhabricatorPolicyCapability::CAN_EDIT); $can_fork = $user->isLoggedIn(); $fork_uri = $this->getApplicationURI('/create/?parent='.$paste->getID()); return id(new PhabricatorActionListView()) ->setUser($user) ->setObject($paste) ->setObjectURI($this->getRequest()->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Fork This Paste')) ->setIcon('fork') ->setDisabled(!$can_fork) ->setWorkflow(!$can_fork) ->setHref($fork_uri)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('View Raw File')) ->setIcon('file') ->setHref($file->getBestURI())) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Paste')) ->setIcon('edit') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($this->getApplicationURI('/edit/'.$paste->getID().'/'))); } private function buildPropertyView( PhabricatorPaste $paste, array $child_phids, PhabricatorActionListView $actions) { $user = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($paste) ->setActionList($actions); $properties->addProperty( pht('Author'), $this->getHandle($paste->getAuthorPHID())->renderLink()); $properties->addProperty( pht('Created'), phabricator_datetime($paste->getDateCreated(), $user)); if ($paste->getParentPHID()) { $properties->addProperty( pht('Forked From'), $this->getHandle($paste->getParentPHID())->renderLink()); } if ($child_phids) { $properties->addProperty( pht('Forks'), $this->renderHandlesForPHIDs($child_phids)); } $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $paste); return $properties; } } diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php index f0203edcd..1638f023f 100644 --- a/src/applications/people/controller/PhabricatorPeopleEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleEditController.php @@ -1,841 +1,832 @@ id = idx($data, 'id'); $this->view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $admin = $request->getUser(); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if ($this->id) { $user = id(new PhabricatorUser())->load($this->id); if (!$user) { return new Aphront404Response(); } $base_uri = '/people/edit/'.$user->getID().'/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit User')) - ->setHref('/people/edit/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getFullName()) - ->setHref($base_uri)); + $crumbs->addTextCrumb(pht('Edit User'), '/people/edit/'); + $crumbs->addTextCrumb($user->getFullName(), $base_uri); } else { $user = new PhabricatorUser(); $base_uri = '/people/edit/'; - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create New User')) - ->setHref($base_uri)); + $crumbs->addTextCrumb(pht('Create New User'), $base_uri); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($base_uri)); $nav->addLabel(pht('User Information')); $nav->addFilter('basic', pht('Basic Information')); $nav->addFilter('role', pht('Edit Roles')); $nav->addFilter('cert', pht('Conduit Certificate')); $nav->addFilter('profile', pht('View Profile'), '/p/'.$user->getUsername().'/'); $nav->addLabel(pht('Special')); $nav->addFilter('rename', pht('Change Username')); if ($user->getIsSystemAgent()) { $nav->addFilter('picture', pht('Set Account Picture')); } $nav->addFilter('delete', pht('Delete User')); if (!$user->getID()) { $this->view = 'basic'; } $view = $nav->selectFilter($this->view, 'basic'); $content = array(); if ($request->getStr('saved')) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Changes Saved')); $notice->appendChild( phutil_tag('p', array(), pht('Your changes were saved.'))); $content[] = $notice; } switch ($view) { case 'basic': $response = $this->processBasicRequest($user); break; case 'role': $response = $this->processRoleRequest($user); break; case 'cert': $response = $this->processCertificateRequest($user); break; case 'rename': $response = $this->processRenameRequest($user); break; case 'picture': $response = $this->processSetAccountPicture($user); break; case 'delete': $response = $this->processDeleteRequest($user); break; default: return new Aphront404Response(); } if ($response instanceof AphrontResponse) { return $response; } $content[] = $response; if ($user->getID()) { $nav->appendChild($content); } else { $nav = $this->buildSideNavView(); $nav->selectFilter('edit'); $nav->appendChild($content); } $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Edit User'), 'device' => true, )); } private function processBasicRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $e_username = true; $e_realname = true; $e_email = true; $errors = array(); $welcome_checked = true; $new_email = null; $request = $this->getRequest(); if ($request->isFormPost()) { $welcome_checked = $request->getInt('welcome'); $is_new = !$user->getID(); if ($is_new) { $user->setUsername($request->getStr('username')); $new_email = $request->getStr('email'); if (!strlen($new_email)) { $errors[] = pht('Email is required.'); $e_email = pht('Required'); } else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } $user->setRealName($request->getStr('realname')); if (!strlen($user->getUsername())) { $errors[] = pht("Username is required."); $e_username = pht('Required'); } else if (!PhabricatorUser::validateUsername($user->getUsername())) { $errors[] = PhabricatorUser::describeValidUsername(); $e_username = pht('Invalid'); } else { $e_username = null; } if (!strlen($user->getRealName())) { $errors[] = pht('Real name is required.'); $e_realname = pht('Required'); } else { $e_realname = null; } if (!$errors) { try { if (!$is_new) { id(new PhabricatorUserEditor()) ->setActor($admin) ->updateUser($user); } else { $email = id(new PhabricatorUserEmail()) ->setAddress($new_email) ->setIsVerified(0); // Automatically approve the user, since an admin is creating them. $user->setIsApproved(1); id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email); if ($request->getStr('role') == 'agent') { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeSystemAgentUser($user, true); } } if ($welcome_checked) { $user->sendWelcomeEmail($admin); } $response = id(new AphrontRedirectResponse()) ->setURI('/people/edit/'.$user->getID().'/?saved=true'); return $response; } catch (AphrontQueryDuplicateKeyException $ex) { $errors[] = pht('Username and email must be unique.'); $same_username = id(new PhabricatorUser()) ->loadOneWhere('username = %s', $user->getUsername()); $same_email = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $new_email); if ($same_username) { $e_username = pht('Duplicate'); } if ($same_email) { $e_email = pht('Duplicate'); } } } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = new AphrontFormView(); $form->setUser($admin); if ($user->getID()) { $form->setAction('/people/edit/'.$user->getID().'/'); } else { $form->setAction('/people/edit/'); } if ($user->getID()) { $is_immutable = true; } else { $is_immutable = false; } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setName('username') ->setValue($user->getUsername()) ->setError($e_username) ->setDisabled($is_immutable)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realname') ->setValue($user->getRealName()) ->setError($e_realname)); if (!$user->getID()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setDisabled($is_immutable) ->setValue($new_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } else { $email = $user->loadPrimaryEmail(); if ($email) { $status = $email->getIsVerified() ? pht('Verified') : pht('Unverified'); } else { $status = pht('No Email Address'); } $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Email')) ->setValue($status)); $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, pht('Re-send "Welcome to Phabricator" email.'), false)); } $form->appendChild($this->getRoleInstructions()); if (!$user->getID()) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Role')) ->setName('role') ->setValue('user') ->setOptions( array( 'user' => pht('Normal User'), 'agent' => pht('System Agent'), )) ->setCaption( pht('You can create a "system agent" account for bots, '. 'scripts, etc.'))) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, pht('Send "Welcome to Phabricator" email.'), $welcome_checked)); } else { $roles = array(); if ($user->getIsSystemAgent()) { $roles[] = pht('System Agent'); } if ($user->getIsAdmin()) { $roles[] = pht('Admin'); } if ($user->getIsDisabled()) { $roles[] = pht('Disabled'); } if (!$user->getIsApproved()) { $roles[] = pht('Not Approved'); } if (!$roles) { $roles[] = pht('Normal User'); } $roles = implode(', ', $roles); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Roles')) ->setValue($roles)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))); if ($user->getID()) { $title = pht('Edit User'); } else { $title = pht('Create New User'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); return array($form_box); } private function processRoleRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $is_self = ($user->getID() == $admin->getID()); $errors = array(); if ($request->isFormPost()) { $log_template = PhabricatorUserLog::newLog( $admin, $user, null); $logs = array(); if ($is_self) { $errors[] = pht("You can not edit your own role."); } else { $new_admin = (bool)$request->getBool('is_admin'); $old_admin = (bool)$user->getIsAdmin(); if ($new_admin != $old_admin) { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeAdminUser($user, $new_admin); } $new_disabled = (bool)$request->getBool('is_disabled'); $old_disabled = (bool)$user->getIsDisabled(); if ($new_disabled != $old_disabled) { id(new PhabricatorUserEditor()) ->setActor($admin) ->disableUser($user, $new_disabled); } } if (!$errors) { return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('saved', 'true')); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($admin) ->setAction($request->getRequestURI()->alter('saved', null)); if ($is_self) { $inst = pht('NOTE: You can not edit your own role.'); $form->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); } $form ->appendChild($this->getRoleInstructions()) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_admin', 1, pht('Administrator'), $user->getIsAdmin()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_disabled', 1, pht('Disabled'), $user->getIsDisabled()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_agent', 1, pht('System Agent (Bot/Script User)'), $user->getIsSystemAgent()) ->setDisabled(true)); if (!$is_self) { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Edit Role'))); } $title = pht('Edit Role'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); return array($form_box); } private function processCertificateRequest($user) { $request = $this->getRequest(); $admin = $request->getUser(); $inst = pht('You can use this certificate '. 'to write scripts or bots which interface with Phabricator over '. 'Conduit.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); if ($user->getIsSystemAgent()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Certificate')) ->setValue($user->getConduitCertificate())); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Certificate')) ->setValue( pht('You may only view the certificates of System Agents.'))); } $title = pht('Conduit Certificate'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return array($form_box); } private function processRenameRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $e_username = true; $username = $user->getUsername(); $errors = array(); if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = pht('Required'); $errors[] = pht('New username is required.'); } else if ($username == $user->getUsername()) { $e_username = pht('Invalid'); $errors[] = pht('New username must be different from old username.'); } else if (!PhabricatorUser::validateUsername($username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } if (!$errors) { try { id(new PhabricatorUserEditor()) ->setActor($admin) ->changeUsername($user, $username); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('saved', true)); } catch (AphrontQueryDuplicateKeyException $ex) { $e_username = pht('Not Unique'); $errors[] = pht('Another user already has that username.'); } } } if ($errors) { $errors = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } else { $errors = null; } $inst1 = pht('Be careful when renaming users!'); $inst2 = pht('The old username will no longer be tied to the user, so '. 'anything which uses it (like old commit messages) will no longer '. 'associate correctly. And if you give a user a username which some '. 'other user used to have, username lookups will begin returning '. 'the wrong user.'); $inst3 = pht('It is generally safe to rename newly created users (and '. 'test users and so on), but less safe to rename established users '. 'and unsafe to reissue a username.'); $inst4 = pht('Users who rely on password auth will need to reset their '. 'passwordafter their username is changed (their username is part '. 'of the salt in the password hash). They will receive an email '. 'with instructions on how to do this.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild(hsprintf( '

    '. '%s '. '%s'. '

    '. '

    '. '%s'. '

    '. '

    '. '%s'. '

    ', $inst1, $inst2, $inst3, $inst4)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Old Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('New Username')) ->setValue($username) ->setName('username') ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Username'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Username')) ->setFormError($errors) ->setForm($form); return array($form_box); } private function processDeleteRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $far1 = pht('As you stare into the gaping maw of the abyss, something '. 'hold you back.'); $far2 = pht('You can not delete your own account.'); if ($user->getPHID() == $admin->getPHID()) { $error = new AphrontErrorView(); $error->setTitle(pht('You Shall Journey No Farther')); $error->appendChild(hsprintf( '

    %s

    %s

    ', $far1, $far2)); return $error; } $e_username = true; $username = null; $errors = array(); if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = pht('Required'); $errors[] = pht('You must type the username to confirm deletion.'); } else if ($username != $user->getUsername()) { $e_username = pht('Invalid'); $errors[] = pht('You must type the username correctly.'); } if (!$errors) { id(new PhabricatorUserEditor()) ->setActor($admin) ->deleteUser($user); return id(new AphrontRedirectResponse())->setURI('/people/'); } } if ($errors) { $errors = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } else { $errors = null; } $str1 = pht('Be careful when deleting users!'); $str2 = pht('If this user interacted with anything, it is generally '. 'better to disable them, not delete them. If you delete them, it will '. 'no longer be possible to search for their objects, for example, '. 'and you will lose other information about their history. Disabling '. 'them instead will prevent them from logging in but not destroy '. 'any of their data.'); $str3 = pht('It is generally safe to delete newly created users (and '. 'test users and so on), but less safe to delete established users. '. 'If possible, disable them instead.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild(hsprintf( '

    '. '%s %s'. '

    '. '

    '. '%s'. '

    ', $str1, $str2, $str3)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Confirm')) ->setValue($username) ->setName('username') ->setCaption(pht("Type the username again to confirm deletion.")) ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Delete User'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Delete User')) ->setFormError($errors) ->setForm($form); return array($form_box); } private function getRoleInstructions() { $roles_link = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'article/User_Guide_Account_Roles.html'), 'target' => '_blank', ), pht('User Guide: Account Roles')); return phutil_tag( 'p', array('class' => 'aphront-form-instructions'), pht('For a detailed explanation of account roles, see %s.', $roles_link)); } private function processSetAccountPicture(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $profile = $user->loadUserProfile(); if (!$profile->getID()) { $profile->setTitle(''); $profile->setBlurb(''); } $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_image = null; $errors = array(); if ($request->isFormPost()) { $default_image = $request->getExists('default_image'); if ($default_image) { $profile->setProfileImagePHID(null); $user->setProfileImagePHID(null); } else if ($request->getFileExists('image')) { $file = null; $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $admin->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); // Generate the large picture for the profile page. $large_xformed = $xformer->executeProfileTransform( $file, $width = 280, $min_height = 140, $max_height = 420); $profile->setProfileImagePHID($large_xformed->getPHID()); // Generate the small picture for comments, etc. $small_xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); $user->setProfileImagePHID($small_xformed->getPHID()); } else { $e_image = pht('Not Supported'); $errors[] = pht('This server only supports these image formats:'). ' ' .implode(', ', $supported_formats); } } if (!$errors) { $user->save(); $profile->save(); $response = id(new AphrontRedirectResponse()) ->setURI('/people/edit/'.$user->getID().'/picture/'); return $response; } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { if ($request->getStr('saved')) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $error_view->setTitle(pht('Changes Saved')); $error_view->appendChild( phutil_tag('p', array(), pht('Your changes have been saved.'))); $error_view = $error_view->render(); } } $img_src = $user->loadProfileImageURI(); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Profile Image')) ->setValue( phutil_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormImageControl()) ->setLabel(pht('Change Image')) ->setName('image') ->setError($e_image) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton('/people/edit/'.$user->getID().'/')); $panel = new AphrontPanelView(); $panel->setHeader(pht('Set Profile Picture')); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->setNoBackground(); $panel->appendChild($form); return array($error_view, $panel); } } diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php index ee6e2cd8d..280971bf0 100644 --- a/src/applications/people/controller/PhabricatorPeopleLdapController.php +++ b/src/applications/people/controller/PhabricatorPeopleLdapController.php @@ -1,219 +1,218 @@ getRequest(); $admin = $request->getUser(); $content = array(); $form = id(new AphrontFormView()) ->setAction($request->getRequestURI() ->alter('search', 'true')->alter('import', null)) ->setUser($admin) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP username')) ->setName('username')) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP query')) ->setCaption(pht('A filter such as (objectClass=*)')) ->setName('query')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Search'))); $panel = id(new AphrontPanelView()) ->setHeader(pht('Import LDAP Users')) ->setNoBackground() ->setWidth(AphrontPanelView::WIDTH_FORM) ->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Import Ldap Users')) - ->setHref($this->getApplicationURI('/ldap/'))); + $crumbs->addTextCrumb( + pht('Import Ldap Users'), + $this->getApplicationURI('/ldap/')); $nav = $this->buildSideNavView(); $nav->setCrumbs($crumbs); $nav->selectFilter('ldap'); $nav->appendChild($content); if ($request->getStr('import')) { $nav->appendChild($this->processImportRequest($request)); } $nav->appendChild($panel); if ($request->getStr('search')) { $nav->appendChild($this->processSearchRequest($request)); } return $this->buildApplicationPage( $nav, array( 'title' => pht('Import Ldap Users'), 'device' => true, )); } private function processImportRequest($request) { $admin = $request->getUser(); $usernames = $request->getArr('usernames'); $emails = $request->getArr('email'); $names = $request->getArr('name'); $notice_view = new AphrontErrorView(); $notice_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice_view->setTitle(pht("Import Successful")); $notice_view->setErrors(array( pht("Successfully imported users from LDAP"), )); $list = new PHUIObjectItemListView(); $list->setNoDataString(pht("No users imported?")); foreach ($usernames as $username) { $user = new PhabricatorUser(); $user->setUsername($username); $user->setRealname($names[$username]); $email_obj = id(new PhabricatorUserEmail()) ->setAddress($emails[$username]) ->setIsVerified(1); try { id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email_obj); id(new PhabricatorExternalAccount()) ->setUserPHID($user->getPHID()) ->setAccountType('ldap') ->setAccountDomain('self') ->setAccountID($username) ->save(); $header = pht('Successfully added %s', $username); $attribute = null; $color = 'green'; } catch (Exception $ex) { $header = pht('Failed to add %s', $username); $attribute = $ex->getMessage(); $color = 'red'; } $item = id(new PHUIObjectItemView()) ->setHeader($header) ->addAttribute($attribute) ->setBarColor($color); $list->addItem($item); } return array( $notice_view, $list, ); } private function processSearchRequest($request) { $panel = new AphrontPanelView(); $admin = $request->getUser(); $search = $request->getStr('query'); $ldap_provider = PhabricatorAuthProviderLDAP::getLDAPProvider(); if (!$ldap_provider) { throw new Exception("No LDAP provider enabled!"); } $ldap_adapter = $ldap_provider->getAdapter(); $ldap_adapter->setLoginUsername($request->getStr('username')); $ldap_adapter->setLoginPassword( new PhutilOpaqueEnvelope($request->getStr('password'))); // This causes us to connect and bind. // TODO: Clean up this discard mode stuff. DarkConsoleErrorLogPluginAPI::enableDiscardMode(); $ldap_adapter->getAccountID(); DarkConsoleErrorLogPluginAPI::disableDiscardMode(); $results = $ldap_adapter->searchLDAP('%Q', $search); foreach ($results as $key => $record) { $account_id = $ldap_adapter->readLDAPRecordAccountID($record); if (!$account_id) { unset($results[$key]); continue; } $info = array( $account_id, $ldap_adapter->readLDAPRecordEmail($record), $ldap_adapter->readLDAPRecordRealName($record), ); $results[$key] = $info; $results[$key][] = $this->renderUserInputs($info); } $form = id(new AphrontFormView()) ->setUser($admin); $table = new AphrontTableView($results); $table->setHeaders( array( pht('Username'), pht('Email'), pht('Real Name'), pht('Import?'), )); $form->appendChild($table); $form->setAction($request->getRequestURI() ->alter('import', 'true')->alter('search', null)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Import'))); $panel->appendChild($form); return $panel; } private function renderUserInputs($user) { $username = $user[0]; return hsprintf( '%s%s%s', phutil_tag( 'input', array( 'type' => 'checkbox', 'name' => 'usernames[]', 'value' => $username, )), phutil_tag( 'input', array( 'type' => 'hidden', 'name' => "email[$username]", 'value' => $user[1], )), phutil_tag( 'input', array( 'type' => 'hidden', 'name' => "name[$username]", 'value' => $user[2], ))); } } diff --git a/src/applications/people/controller/PhabricatorPeopleLogsController.php b/src/applications/people/controller/PhabricatorPeopleLogsController.php index f43be11c2..8cd7ad57a 100644 --- a/src/applications/people/controller/PhabricatorPeopleLogsController.php +++ b/src/applications/people/controller/PhabricatorPeopleLogsController.php @@ -1,231 +1,228 @@ getRequest(); $user = $request->getUser(); $filter_activity = $request->getStr('activity'); $filter_ip = $request->getStr('ip'); $filter_session = $request->getStr('session'); $filter_user = $request->getArr('user', array()); $filter_actor = $request->getArr('actor', array()); $user_value = array(); $actor_value = array(); $phids = array_merge($filter_user, $filter_actor); if ($phids) { $handles = $this->loadViewerHandles($phids); if ($filter_user) { $filter_user = reset($filter_user); $user_value = array($handles[$filter_user]); } if ($filter_actor) { $filter_actor = reset($filter_actor); $actor_value = array($handles[$filter_actor]); } } $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Filter Actor')) ->setName('actor') ->setLimit(1) ->setValue($actor_value) ->setDatasource('/typeahead/common/accounts/')) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Filter User')) ->setName('user') ->setLimit(1) ->setValue($user_value) ->setDatasource('/typeahead/common/accounts/')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Show Activity')) ->setName('activity') ->setValue($filter_activity) ->setOptions( array( '' => pht('All Activity'), 'admin' => pht('Admin Activity'), ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Filter IP')) ->setName('ip') ->setValue($filter_ip) ->setCaption( pht('Enter an IP (or IP prefix) to show only activity by that '. 'remote address.'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Filter Session')) ->setName('session') ->setValue($filter_session)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Filter Logs'))); $log_table = new PhabricatorUserLog(); $conn_r = $log_table->establishConnection('r'); $where_clause = array(); $where_clause[] = '1 = 1'; if ($filter_user) { $where_clause[] = qsprintf( $conn_r, 'userPHID = %s', $filter_user); } if ($filter_actor) { $where_clause[] = qsprintf( $conn_r, 'actorPHID = %s', $filter_actor); } if ($filter_activity == 'admin') { $where_clause[] = qsprintf( $conn_r, 'action NOT IN (%Ls)', array( PhabricatorUserLog::ACTION_LOGIN, PhabricatorUserLog::ACTION_LOGOUT, PhabricatorUserLog::ACTION_LOGIN_FAILURE, )); } if ($filter_ip) { $where_clause[] = qsprintf( $conn_r, 'remoteAddr LIKE %>', $filter_ip); } if ($filter_session) { $where_clause[] = qsprintf( $conn_r, 'session = %s', $filter_session); } $where_clause = '('.implode(') AND (', $where_clause).')'; $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'page'); $pager->setOffset($request->getInt('page')); $pager->setPageSize(500); $logs = $log_table->loadAllWhere( '(%Q) ORDER BY dateCreated DESC LIMIT %d, %d', $where_clause, $pager->getOffset(), $pager->getPageSize() + 1); $logs = $pager->sliceResults($logs); $phids = array(); foreach ($logs as $log) { $phids[$log->getActorPHID()] = true; $phids[$log->getUserPHID()] = true; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $rows = array(); foreach ($logs as $log) { $rows[] = array( phabricator_date($log->getDateCreated(), $user), phabricator_time($log->getDateCreated(), $user), $log->getAction(), $log->getActorPHID() ? $handles[$log->getActorPHID()]->getName() : null, $handles[$log->getUserPHID()]->getName(), json_encode($log->getOldValue(), true), json_encode($log->getNewValue(), true), phutil_tag( 'a', array( 'href' => $request ->getRequestURI() ->alter('ip', $log->getRemoteAddr()), ), $log->getRemoteAddr()), phutil_tag( 'a', array( 'href' => $request ->getRequestURI() ->alter('session', $log->getSession()), ), $log->getSession()), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Date'), pht('Time'), pht('Action'), pht('Actor'), pht('User'), pht('Old'), pht('New'), pht('IP'), pht('Session'), )); $table->setColumnClasses( array( '', 'right', '', '', '', 'wrap', 'wrap', '', 'wide', )); $panel = new AphrontPanelView(); $panel->setHeader(pht('Activity Logs')); $panel->setNoBackground(); $panel->appendChild($table); $panel->appendChild($pager); $filter = new AphrontListFilterView(); $filter->appendChild($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Activity Logs')) - ->setHref('/people/logs/')); + $crumbs->addTextCrumb(pht('Activity Logs'), '/people/logs/'); $nav = $this->buildSideNavView(); $nav->selectFilter('logs'); $nav->appendChild( array( $filter, $panel, )); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Activity Logs'), 'device' => true, )); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 261d2f27a..cc17cf4bb 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -1,134 +1,132 @@ username = idx($data, 'username'); } public function processRequest() { $viewer = $this->getRequest()->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($this->username)) ->needProfileImage(true) ->executeOne(); if (!$user) { return new Aphront404Response(); } require_celerity_resource('phabricator-profile-css'); $profile = $user->loadUserProfile(); $username = phutil_escape_uri($user->getUserName()); $picture = $user->loadProfileImageURI(); $header = id(new PHUIHeaderView()) ->setHeader($user->getUserName().' ('.$user->getRealName().')') ->setSubheader($profile->getTitle()) ->setImage($picture); $actions = id(new PhabricatorActionListView()) ->setObject($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($viewer); $can_edit = ($user->getPHID() == $viewer->getPHID()); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Profile')) ->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('image') ->setName(pht('Edit Profile Picture')) ->setHref($this->getApplicationURI('picture/'.$user->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($viewer->getIsAdmin()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('blame') ->setName(pht('Administrate User')) ->setHref($this->getApplicationURI('edit/'.$user->getID().'/'))); } $properties = $this->buildPropertyView($user, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername())); + $crumbs->addTextCrumb($user->getUsername()); $feed = $this->renderUserFeed($user); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $feed, ), array( 'title' => $user->getUsername(), 'device' => true, )); } private function buildPropertyView( PhabricatorUser $user, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($user) ->setActionList($actions); $field_list = PhabricatorCustomField::getObjectFields( $user, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($user, $viewer, $view); return $view; } private function renderUserFeed(PhabricatorUser $user) { $viewer = $this->getRequest()->getUser(); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $user->getPHID(), )); $query->setLimit(100); $query->setViewer($viewer); $stories = $query->execute(); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($viewer); $builder->setShowHovercards(true); $view = $builder->buildView(); return phutil_tag_div( 'profile-feed profile-wrap-responsive', $view->render()); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php index f341f1db7..5c5ff5891 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileEditController.php @@ -1,98 +1,93 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $profile_uri = '/p/'.$user->getUsername().'/'; $field_list = PhabricatorCustomField::getObjectFields( $user, PhabricatorCustomField::ROLE_EDIT); $field_list ->setViewer($user) ->readFieldsFromStorage($user); $validation_exception = null; if ($request->isFormPost()) { $xactions = $field_list->buildFieldTransactionsFromRequest( new PhabricatorUserTransaction(), $request); $editor = id(new PhabricatorUserProfileEditor()) ->setActor($viewer) ->setContentSource( PhabricatorContentSource::newFromRequest($request)) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($user, $xactions); return id(new AphrontRedirectResponse())->setURI($profile_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } $title = pht('Edit Profile'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername()) - ->setHref($profile_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($user->getUsername(), $profile_uri); + $crumbs->addTextCrumb($title); $form = id(new AphrontFormView()) ->setUser($viewer); $field_list->appendFieldsToForm($form); $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Save Profile'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Your Profile')) ->setValidationException($validation_exception) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index 169b90911..91405ea4a 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -1,308 +1,303 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $profile_uri = '/p/'.$user->getUsername().'/'; $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new profile picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { if ($is_default) { $user->setProfileImagePHID(null); } else { $user->setProfileImagePHID($xformed->getPHID()); $xformed->attachToObject($viewer, $user->getPHID()); } $user->save(); return id(new AphrontRedirectResponse())->setURI($profile_uri); } } $title = pht('Edit Profile Picture'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($user->getUsername()) - ->setHref($profile_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($user->getUsername(), $profile_uri); + $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png'); $images = array(); $current = $user->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } // Try to add external account images for any associated external accounts. $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) ->needImages(true) ->execute(); foreach ($accounts as $account) { $file = $account->getProfileImageFile(); if ($account->getProfileImagePHID() != $file->getPHID()) { // This is a default image, just skip it. continue; } $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $account->getProviderKey()); if ($provider) { $tip = pht('Picture From %s', $provider->getProviderName()); } else { $tip = pht('Picture From External Account'); } if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => $tip, ); } } // Try to add Gravatar images for any email addresses associated with the // account. if (PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $viewer->getPHID()); $futures = array(); foreach ($emails as $email_object) { $email = $email_object->getAddress(); $hash = md5(strtolower(trim($email))); $uri = id(new PhutilURI("https://secure.gravatar.com/avatar/{$hash}")) ->setQueryParams( array( 'size' => 200, 'default' => '404', 'rating' => 'x', )); $futures[$email] = new HTTPSFuture($uri); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (Futures($futures) as $email => $future) { try { list($body) = $future->resolvex(); $file = PhabricatorFile::newFromFileData( $body, array( 'name' => 'profile-gravatar', 'ttl' => (60 * 60 * 4), )); if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Gravatar for %s', $email), ); } } catch (Exception $ex) { // Just continue. } } unset($unguarded); } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), $button); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $upload_form = id(new AphrontFormView()) ->setUser($user) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Upload Picture'))); if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index 1b9aac97d..be2ae64a8 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -1,199 +1,196 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $submit_button = pht('Save Changes'); $page_title = pht('Edit Blog'); $cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/'); } else { $blog = id(new PhameBlog()) ->setCreatorPHID($user->getPHID()); $blog->setViewPolicy(PhabricatorPolicies::POLICY_USER); $blog->setEditPolicy(PhabricatorPolicies::POLICY_USER); $blog->setJoinPolicy(PhabricatorPolicies::POLICY_USER); $submit_button = pht('Create Blog'); $page_title = pht('Create Blog'); $cancel_uri = $this->getApplicationURI(); } $e_name = true; $e_custom_domain = null; $errors = array(); if ($request->isFormPost()) { $name = $request->getStr('name'); $description = $request->getStr('description'); $custom_domain = $request->getStr('custom_domain'); $skin = $request->getStr('skin'); if (empty($name)) { $errors[] = pht('You must give the blog a name.'); $e_name = pht('Required'); } else { $e_name = null; } $blog->setName($name); $blog->setDescription($description); $blog->setDomain(nonempty($custom_domain, null)); $blog->setSkin($skin); if (!empty($custom_domain)) { $error = $blog->validateCustomDomain($custom_domain); if ($error) { $errors[] = $error; $e_custom_domain = pht('Invalid'); } } $blog->setViewPolicy($request->getStr('can_view')); $blog->setEditPolicy($request->getStr('can_edit')); $blog->setJoinPolicy($request->getStr('can_join')); // Don't let users remove their ability to edit blogs. PhabricatorPolicyFilter::mustRetainCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); if (!$errors) { try { $blog->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/')); } catch (AphrontQueryDuplicateKeyException $ex) { $errors[] = pht('Domain must be unique.'); $e_custom_domain = pht('Not Unique'); } } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($blog) ->execute(); $skins = PhameSkinSpecification::loadAllSkinSpecifications(); $skins = mpull($skins, 'getName'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($blog->getName()) ->setID('blog-name') ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($blog->getDescription()) ->setID('blog-description') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_join')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Custom Domain')) ->setName('custom_domain') ->setValue($blog->getDomain()) ->setCaption( pht('Must include at least one dot (.), e.g. blog.example.com')) ->setError($e_custom_domain)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Skin')) ->setName('skin') ->setValue($blog->getSkin()) ->setOptions($skins)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } else { $error_view = null; } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($page_title) - ->setHref($this->getApplicationURI('blog/new'))); + $crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new')); $nav = $this->renderSideNavFilterView(); $nav->selectFilter($this->id ? null : 'blog/new'); $nav->appendChild( array( $crumbs, $form_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, 'device' => true, )); } } diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php index bd11cf294..ccba6c860 100644 --- a/src/applications/phame/controller/blog/PhameBlogListController.php +++ b/src/applications/phame/controller/blog/PhameBlogListController.php @@ -1,91 +1,88 @@ filter = idx($data, 'filter'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $nav = $this->renderSideNavFilterView(null); $filter = $nav->selectFilter('blog/'.$this->filter, 'blog/user'); $query = id(new PhameBlogQuery()) ->setViewer($user); switch ($filter) { case 'blog/all': $title = pht('All Blogs'); $nodata = pht('No blogs have been created.'); break; case 'blog/user': $title = pht('Joinable Blogs'); $nodata = pht('There are no blogs you can contribute to.'); $query->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )); break; default: throw new Exception("Unknown filter '{$filter}'!"); } $pager = id(new AphrontPagerView()) ->setURI($request->getRequestURI(), 'offset') ->setOffset($request->getInt('offset')); $blogs = $query->executeWithOffsetPager($pager); $blog_list = $this->renderBlogList($blogs, $user, $nodata); $blog_list->setPager($pager); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( $crumbs, $blog_list, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } private function renderBlogList( array $blogs, PhabricatorUser $user, $nodata) { $view = new PHUIObjectItemListView(); $view->setNoDataString($nodata); $view->setUser($user); foreach ($blogs as $blog) { $item = id(new PHUIObjectItemView()) ->setHeader($blog->getName()) ->setHref($this->getApplicationURI('blog/view/'.$blog->getID().'/')) ->setObject($blog); $view->addItem($item); } return $view; } } diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index 7efb21b3b..5bd9fabbb 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -1,201 +1,198 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $posts = id(new PhamePostQuery()) ->setViewer($user) ->withBlogPHIDs(array($blog->getPHID())) ->executeWithCursorPager($pager); $nav = $this->renderSideNavFilterView(null); $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) ->setUser($user) ->setPolicyObject($blog); $handle_phids = array_merge( mpull($posts, 'getBloggerPHID'), mpull($posts, 'getBlogPHID')); $this->loadHandles($handle_phids); $actions = $this->renderActions($blog, $user); $properties = $this->renderProperties($blog, $user, $actions); $post_list = $this->renderPostList( $posts, $user, pht('This blog has no visible posts.')); require_celerity_resource('phame-css'); $post_list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->addClass('phame-post-list') ->appendChild($post_list); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($blog->getName()) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($blog->getName(), $this->getApplicationURI()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $nav->appendChild( array( $crumbs, $object_box, $post_list, )); return $this->buildApplicationPage( $nav, array( 'device' => true, 'title' => $blog->getName(), )); } private function renderProperties( PhameBlog $blog, PhabricatorUser $user, PhabricatorActionListView $actions) { require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); $properties = new PHUIPropertyListView(); $properties->setActionList($actions); $properties->addProperty( pht('Skin'), $blog->getSkin()); $properties->addProperty( pht('Domain'), $blog->getDomain()); $feed_uri = PhabricatorEnv::getProductionURI( $this->getApplicationURI('blog/feed/'.$blog->getID().'/')); $properties->addProperty( pht('Atom URI'), javelin_tag('a', array( 'href' => $feed_uri, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Atom URI does not support custom domains.'), 'size' => 320, ) ), $feed_uri)); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $blog); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $properties->addProperty( pht('Joinable By'), $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user) ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) ->process(); $properties->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION))); return $properties; } private function renderActions(PhameBlog $blog, PhabricatorUser $user) { $actions = id(new PhabricatorActionListView()) ->setObject($blog) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($user); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); $can_join = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_JOIN); $must_use_form = $blog->getDomain(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('new') ->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID())) ->setName(pht('Write Post')) ->setDisabled(!$can_join) ->setWorkflow(!$can_join)); $actions->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setIcon('world') ->setHref($this->getApplicationURI('live/'.$blog->getID().'/')) ->setRenderAsForm($must_use_form) ->setName(pht('View Live'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/')) ->setName('Edit Blog') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('delete') ->setHref($this->getApplicationURI('blog/delete/'.$blog->getID().'/')) ->setName('Delete Blog') ->setDisabled(!$can_edit) ->setWorkflow(true)); return $actions; } } diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 06e80891e..2dfc479bc 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -1,201 +1,200 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI('/post/view/'.$this->id.'/'); $submit_button = pht('Save Changes'); $page_title = pht('Edit Post'); } else { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($request->getInt('blog'))) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $post = id(new PhamePost()) ->setBloggerPHID($user->getPHID()) ->setBlogPHID($blog->getPHID()) ->setBlog($blog) ->setDatePublished(0) ->setVisibility(PhamePost::VISIBILITY_DRAFT); $cancel_uri = $this->getApplicationURI('/blog/view/'.$blog->getID().'/'); $submit_button = pht('Save Draft'); $page_title = pht('Create Post'); } $e_phame_title = null; $e_title = true; $errors = array(); if ($request->isFormPost()) { $comments = $request->getStr('comments_widget'); $data = array('comments_widget' => $comments); $phame_title = $request->getStr('phame_title'); $phame_title = PhabricatorSlug::normalize($phame_title); $title = $request->getStr('title'); $post->setTitle($title); $post->setPhameTitle($phame_title); $post->setBody($request->getStr('body')); $post->setConfigData($data); if ($phame_title == '/') { $errors[] = pht('Phame title must be nonempty.'); $e_phame_title = pht('Required'); } if (!strlen($title)) { $errors[] = pht('Title must be nonempty.'); $e_title = pht('Required'); } else { $e_title = null; } if (!$errors) { try { $post->save(); $uri = $this->getApplicationURI('/post/view/'.$post->getID().'/'); return id(new AphrontRedirectResponse())->setURI($uri); } catch (AphrontQueryDuplicateKeyException $e) { $e_phame_title = pht('Not Unique'); $errors[] = pht('Another post already uses this slug. '. 'Each post must have a unique slug.'); } } } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($post->getBlogPHID())) ->executeOne(); $form = id(new AphrontFormView()) ->setUser($user) ->addHiddenInput('blog', $request->getInt('blog')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Blog')) ->setValue($handle->renderLink())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($post->getTitle()) ->setID('post-title') ->setError($e_title)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phame Title')) ->setName('phame_title') ->setValue(rtrim($post->getPhameTitle(), '/')) ->setID('post-phame-title') ->setCaption(pht('Up to 64 alphanumeric characters '. 'with underscores for spaces. '. 'Formatting is enforced.')) ->setError($e_phame_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Body')) ->setName('body') ->setValue($post->getBody()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setID('post-body') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Comments Widget')) ->setName('comments_widget') ->setvalue($post->getCommentsWidget()) ->setOptions($post->getCommentsWidgetOptionsForSelect())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $loading = phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div('aphront-panel-preview', array( phutil_tag_div('phame-post-preview-header', pht('Post Preview')), phutil_tag('div', array('id' => 'post-preview'), $loading), )); require_celerity_resource('phame-css'); Javelin::initBehavior( 'phame-post-preview', array( 'preview' => 'post-preview', 'body' => 'post-body', 'title' => 'post-title', 'phame_title' => 'post-phame-title', 'uri' => '/phame/post/preview/', )); if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Errors saving post.')) ->setErrors($errors); } else { $error_view = null; } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($page_title) - ->setHref($this->getApplicationURI('/post/view/'.$this->id.'/'))); + $crumbs->addTextCrumb( + $page_title, + $this->getApplicationURI('/post/view/'.$this->id.'/')); $nav = $this->renderSideNavFilterView(null); $nav->appendChild( array( $crumbs, $form_box, $preview_panel, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, 'device' => true, )); } } diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php index 5153c1dd3..de2418c30 100644 --- a/src/applications/phame/controller/post/PhamePostListController.php +++ b/src/applications/phame/controller/post/PhamePostListController.php @@ -1,101 +1,98 @@ filter = idx($data, 'filter', 'blogger'); $this->bloggername = idx($data, 'bloggername'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new PhamePostQuery()) ->setViewer($user); $nav = $this->renderSideNavFilterView(); switch ($this->filter) { case 'draft': $query->withBloggerPHIDs(array($user->getPHID())); $query->withVisibility(PhamePost::VISIBILITY_DRAFT); $nodata = pht('You have no unpublished drafts.'); $title = pht('Unpublished Drafts'); $nav->selectFilter('post/draft'); break; case 'blogger': if ($this->bloggername) { $blogger = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $this->bloggername); if (!$blogger) { return new Aphront404Response(); } } else { $blogger = $user; } $query->withBloggerPHIDs(array($blogger->getPHID())); if ($blogger->getPHID() == $user->getPHID()) { $nav->selectFilter('post'); $nodata = pht('You have not written any posts.'); } else { $nodata = pht('%s has not written any posts.', $blogger); } $title = pht('Posts By %s', $blogger); break; case 'all': $nodata = pht('There are no visible posts.'); $title = pht('Posts'); $nav->selectFilter('post/all'); break; default: throw new Exception("Unknown filter '{$this->filter}'!"); } $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $posts = $query->executeWithCursorPager($pager); $handle_phids = array_merge( mpull($posts, 'getBloggerPHID'), mpull($posts, 'getBlogPHID')); $this->loadHandles($handle_phids); require_celerity_resource('phame-css'); $post_list = $this->renderPostList($posts, $user, $nodata); $post_list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->addClass('phame-post-list') ->appendChild($post_list); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $nav->appendChild( array( $crumbs, $post_list, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phame/controller/post/PhamePostNewController.php b/src/applications/phame/controller/post/PhamePostNewController.php index 7667aa5fc..b2839c880 100644 --- a/src/applications/phame/controller/post/PhamePostNewController.php +++ b/src/applications/phame/controller/post/PhamePostNewController.php @@ -1,134 +1,131 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $post = null; $view_uri = null; if ($this->id) { $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $view_uri = '/post/view/'.$post->getID().'/'; $view_uri = $this->getApplicationURI($view_uri); if ($request->isFormPost()) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($request->getInt('blog'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )) ->executeOne(); if ($blog) { $post->setBlogPHID($blog->getPHID()); $post->save(); return id(new AphrontRedirectResponse())->setURI($view_uri); } } $title = pht('Move Post'); } else { $title = pht('Create Post'); $view_uri = $this->getApplicationURI('/post/new'); } $blogs = id(new PhameBlogQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_JOIN, )) ->execute(); $nav = $this->renderSideNavFilterView(); $nav->selectFilter('post/new'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($view_uri)); + $crumbs->addTextCrumb($title, $view_uri); $nav->appendChild($crumbs); if (!$blogs) { $notification = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NODATA) ->appendChild( pht('You do not have permission to join any blogs. Create a blog '. 'first, then you can post to it.')); $nav->appendChild($notification); } else { $options = mpull($blogs, 'getName', 'getID'); asort($options); $selected_value = null; if ($post && $post->getBlog()) { $selected_value = $post->getBlog()->getID(); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Blog')) ->setName('blog') ->setOptions($options) ->setValue($selected_value)); if ($post) { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Move Post')) ->addCancelButton($view_uri)); } else { $form ->setAction($this->getApplicationURI('post/edit/')) ->setMethod('GET') ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Continue'))); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); $nav->appendChild($form_box); } return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phame/controller/post/PhamePostPublishController.php b/src/applications/phame/controller/post/PhamePostPublishController.php index ce336b263..6986c4e28 100644 --- a/src/applications/phame/controller/post/PhamePostPublishController.php +++ b/src/applications/phame/controller/post/PhamePostPublishController.php @@ -1,93 +1,90 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('/post/view/'.$post->getID().'/'); if ($request->isFormPost()) { $post->setVisibility(PhamePost::VISIBILITY_PUBLISHED); $post->setDatePublished(time()); $post->save(); return id(new AphrontRedirectResponse())->setURI($view_uri); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Publish Post')) ->addCancelButton($view_uri)); $frame = $this->renderPreviewFrame($post); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Preview Post')) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Preview')) - ->setHref($view_uri)); + $crumbs->addTextCrumb(pht('Preview'), $view_uri); $nav = $this->renderSideNavFilterView(null); $nav->appendChild( array( $crumbs, $form_box, $frame, )); return $this->buildApplicationPage( $nav, array( 'title' => pht('Preview Post'), 'device' => true, )); } private function renderPreviewFrame(PhamePost $post) { // TODO: Clean up this CSS. return phutil_tag( 'div', array( 'style' => 'text-align: center; padding: 1em;', ), phutil_tag( 'iframe', array( 'style' => 'width: 100%; height: 600px; '. 'border: 1px solid #303030;', 'src' => $this->getApplicationURI('/post/framed/'.$post->getID().'/'), ), '')); } } diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 1fb600544..bac0a370d 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -1,210 +1,209 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$post) { return new Aphront404Response(); } $nav = $this->renderSideNavFilterView(); $this->loadHandles( array( $post->getBlogPHID(), $post->getBloggerPHID(), )); $actions = $this->renderActions($post, $user); $properties = $this->renderProperties($post, $user, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($post->getTitle()) - ->setHref($this->getApplicationURI('post/view/'.$post->getID().'/'))); + $crumbs->addTextCrumb( + $post->getTitle(), + $this->getApplicationURI('post/view/'.$post->getID().'/')); $nav->appendChild($crumbs); $header = id(new PHUIHeaderView()) ->setHeader($post->getTitle()) ->setUser($user) ->setPolicyObject($post); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($post->isDraft()) { $object_box->appendChild( id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Draft Post')) ->appendChild( pht('Only you can see this draft until you publish it. '. 'Use "Preview / Publish" to publish this post.'))); } if (!$post->getBlog()) { $object_box->appendChild( id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle(pht('Not On A Blog')) ->appendChild( pht('This post is not associated with a blog (the blog may have '. 'been deleted). Use "Move Post" to move it to a new blog.'))); } $nav->appendChild( array( $object_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $post->getTitle(), 'device' => true, )); } private function renderActions( PhamePost $post, PhabricatorUser $user) { $actions = id(new PhabricatorActionListView()) ->setObject($post) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($user); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $post, PhabricatorPolicyCapability::CAN_EDIT); $id = $post->getID(); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setHref($this->getApplicationURI('post/edit/'.$id.'/')) ->setName(pht('Edit Post')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('move') ->setHref($this->getApplicationURI('post/move/'.$id.'/')) ->setName(pht('Move Post')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($post->isDraft()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('preview') ->setHref($this->getApplicationURI('post/publish/'.$id.'/')) ->setName(pht('Preview / Publish'))); } else { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('unpublish') ->setHref($this->getApplicationURI('post/unpublish/'.$id.'/')) ->setName(pht('Unpublish')) ->setWorkflow(true)); } $actions->addAction( id(new PhabricatorActionView()) ->setIcon('delete') ->setHref($this->getApplicationURI('post/delete/'.$id.'/')) ->setName(pht('Delete Post')) ->setDisabled(!$can_edit) ->setWorkflow(true)); $blog = $post->getBlog(); $can_view_live = $blog && !$post->isDraft(); $must_use_form = $blog && $blog->getDomain(); if ($can_view_live) { $live_uri = 'live/'.$blog->getID().'/post/'.$post->getPhameTitle(); } else { $live_uri = 'post/notlive/'.$post->getID().'/'; } $live_uri = $this->getApplicationURI($live_uri); $actions->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setIcon('world') ->setHref($live_uri) ->setName(pht('View Live')) ->setRenderAsForm($must_use_form) ->setDisabled(!$can_view_live) ->setWorkflow(!$can_view_live)); return $actions; } private function renderProperties( PhamePost $post, PhabricatorUser $user, PhabricatorActionListView $actions) { $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($post) ->setActionList($actions); $properties->addProperty( pht('Blog'), $post->getBlogPHID() ? $this->getHandle($post->getBlogPHID())->renderLink() : null); $properties->addProperty( pht('Blogger'), $this->getHandle($post->getBloggerPHID())->renderLink()); $properties->addProperty( pht('Published'), $post->isDraft() ? pht('Draft') : phabricator_datetime($post->getDatePublished(), $user)); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user) ->addObject($post, PhamePost::MARKUP_FIELD_BODY) ->process(); $properties->invokeWillRenderEvent(); $properties->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))); return $properties; } } diff --git a/src/applications/phlux/controller/PhluxEditController.php b/src/applications/phlux/controller/PhluxEditController.php index 1e847490b..bf162169b 100644 --- a/src/applications/phlux/controller/PhluxEditController.php +++ b/src/applications/phlux/controller/PhluxEditController.php @@ -1,195 +1,189 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $is_new = ($this->key === null); if ($is_new) { $var = new PhluxVariable(); $var->setViewPolicy(PhabricatorPolicies::POLICY_USER); $var->setEditPolicy(PhabricatorPolicies::POLICY_USER); } else { $var = id(new PhluxVariableQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withKeys(array($this->key)) ->executeOne(); if (!$var) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('/view/'.$this->key.'/'); } $e_key = ($is_new ? true : null); $e_value = true; $errors = array(); $key = $var->getVariableKey(); $display_value = null; $value = $var->getVariableValue(); if ($request->isFormPost()) { if ($is_new) { $key = $request->getStr('key'); if (!strlen($key)) { $errors[] = pht('Variable key is required.'); $e_key = pht('Required'); } else if (!preg_match('/^[a-z0-9.-]+$/', $key)) { $errors[] = pht( 'Variable key "%s" must contain only lowercase letters, digits, '. 'period, and hyphen.', $key); $e_key = pht('Invalid'); } } $raw_value = $request->getStr('value'); $value = json_decode($raw_value, true); if ($value === null && strtolower($raw_value) !== 'null') { $e_value = pht('Invalid'); $errors[] = pht('Variable value must be valid JSON.'); $display_value = $raw_value; } if (!$errors) { $editor = id(new PhluxVariableEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = array(); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhluxTransaction::TYPE_EDIT_KEY) ->setNewValue($key); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhluxTransaction::TYPE_EDIT_VALUE) ->setNewValue($value); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); try { $editor->applyTransactions($var, $xactions); $view_uri = $this->getApplicationURI('/view/'.$key.'/'); return id(new AphrontRedirectResponse())->setURI($view_uri); } catch (AphrontQueryDuplicateKeyException $ex) { $e_key = pht('Not Unique'); $errors[] = pht('Variable key must be unique.'); } } } if ($display_value === null) { if (is_array($value) && (array_keys($value) !== array_keys(array_values($value)))) { $json = new PhutilJSON(); $display_value = $json->encodeFormatted($value); } else { $display_value = json_encode($value); } } if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($var) ->execute(); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setValue($var->getVariableKey()) ->setLabel(pht('Key')) ->setName('key') ->setError($e_key) ->setCaption(pht('Lowercase letters, digits, dot and hyphen only.')) ->setDisabled(!$is_new)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setValue($display_value) ->setLabel(pht('Value')) ->setName('value') ->setCaption(pht('Enter value as JSON.')) ->setError($e_value)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($var) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($var) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)); if ($is_new) { $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Variable'))); } else { $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Variable')) ->addCancelButton($view_uri)); } $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $title = pht('Create Variable'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); } else { $title = pht('Edit %s', $this->key); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phlux/controller/PhluxListController.php b/src/applications/phlux/controller/PhluxListController.php index badc28f9c..6c4480bf5 100644 --- a/src/applications/phlux/controller/PhluxListController.php +++ b/src/applications/phlux/controller/PhluxListController.php @@ -1,51 +1,48 @@ getRequest(); $user = $request->getUser(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $query = id(new PhluxVariableQuery()) ->setViewer($user); $vars = $query->executeWithCursorPager($pager); $view = new PHUIObjectItemListView(); foreach ($vars as $var) { $key = $var->getVariableKey(); $item = new PHUIObjectItemView(); $item->setHeader($key); $item->setHref($this->getApplicationURI('/view/'.$key.'/')); $item->addIcon( 'none', phabricator_datetime($var->getDateModified(), $user)); $view->addItem($item); } $crumbs = $this->buildApplicationCrumbs(); $title = pht('Variable List'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $view, $pager, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phlux/controller/PhluxViewController.php b/src/applications/phlux/controller/PhluxViewController.php index c492ba366..1525ef1e1 100644 --- a/src/applications/phlux/controller/PhluxViewController.php +++ b/src/applications/phlux/controller/PhluxViewController.php @@ -1,94 +1,91 @@ key = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $var = id(new PhluxVariableQuery()) ->setViewer($user) ->withKeys(array($this->key)) ->executeOne(); if (!$var) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $title = $var->getVariableKey(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($user) ->setPolicyObject($var); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) ->setObject($var); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $var, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Variable')) ->setHref($this->getApplicationURI('/edit/'.$var->getVariableKey().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $display_value = json_encode($var->getVariableValue()); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($var) ->setActionList($actions) ->addProperty(pht('Value'), $display_value); $xactions = id(new PhluxTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($var->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($var->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $xaction_view, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/pholio/controller/PholioImageHistoryController.php b/src/applications/pholio/controller/PholioImageHistoryController.php index 1581385c6..131c32330 100644 --- a/src/applications/pholio/controller/PholioImageHistoryController.php +++ b/src/applications/pholio/controller/PholioImageHistoryController.php @@ -1,96 +1,90 @@ imageID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $image = id(new PholioImageQuery()) ->setViewer($user) ->withIDs(array($this->imageID)) ->executeOne(); if (!$image) { return new Aphront404Response(); } // note while we have a mock object, its missing images we need to show // the history of what's happened here. // fetch the real deal $mock = id(new PholioMockQuery()) ->setViewer($user) ->needImages(true) ->withIDs(array($image->getMockID())) ->executeOne(); $phids = array($mock->getAuthorPHID()); $this->loadHandles($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); $engine->process(); $images = $mock->getImageHistorySet($this->imageID); $mock->attachImages($images); $latest_image = last($images); $title = pht( 'Image history for "%s" from the mock "%s."', $latest_image->getName(), $mock->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); $comment_form_id = null; $output = id(new PholioMockImagesView()) ->setRequestURI($request->getRequestURI()) ->setCommentFormID($comment_form_id) ->setUser($user) ->setMock($mock) ->setImageID($this->imageID) ->setViewMode('history'); $crumbs = $this->buildApplicationCrumbs(); $crumbs - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('M'.$mock->getID()) - ->setHref('/M'.$mock->getID())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Image History') - ->setHref($request->getRequestURI())); + ->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()) + ->addTextCrumb('Image History', $request->getRequestURI()); $content = array( $crumbs, $header, $output->render(), ); return $this->buildApplicationPage( $content, array( 'title' => 'M'.$mock->getID().' '.$title, 'device' => true, 'pageObjects' => array($mock->getPHID()), )); } } diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index a3dde9a4b..0980a7493 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -1,347 +1,344 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $mock = id(new PholioMockQuery()) ->setViewer($user) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $title = pht('Edit Mock'); $is_new = false; $mock_images = $mock->getImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { $mock = id(new PholioMock()) ->setAuthorPHID($user->getPHID()) ->attachImages(array()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER); $title = pht('Create Mock'); $is_new = true; $files = array(); $mock_images = array(); } $e_name = true; $e_images = true; $errors = array(); $v_name = $mock->getName(); $v_desc = $mock->getDescription(); $v_view = $mock->getViewPolicy(); $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID( $mock->getPHID()); if ($request->isFormPost()) { $xactions = array(); $type_name = PholioTransactionType::TYPE_NAME; $type_desc = PholioTransactionType::TYPE_DESCRIPTION; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS; $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_view = $request->getStr('can_view'); $v_cc = $request->getArr('cc'); $mock_xactions = array(); $mock_xactions[$type_name] = $v_name; $mock_xactions[$type_desc] = $v_desc; $mock_xactions[$type_view] = $v_view; $mock_xactions[$type_cc] = array('=' => $v_cc); if (!strlen($request->getStr('name'))) { $e_name = 'Required'; $errors[] = pht('You must give the mock a name.'); } $file_phids = $request->getArr('file_phids'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $files = array_select_keys($files, $file_phids); } else { $files = array(); } if (!$files) { $e_images = pht('Required'); $errors[] = pht('You must add at least one image to the mock.'); } else { $mock->setCoverPHID(head($files)->getPHID()); } if (!$errors) { foreach ($mock_xactions as $type => $value) { $xactions[$type] = id(new PholioTransaction()) ->setTransactionType($type) ->setNewValue($value); } $order = $request->getStrList('imageOrder'); $sequence_map = array_flip($order); $replaces = $request->getArr('replaces'); $replaces_map = array_flip($replaces); /** * Foreach file posted, check to see whether we are replacing an image, * adding an image, or simply updating image metadata. Create * transactions for these cases as appropos. */ foreach ($files as $file_phid => $file) { $replaces_image_phid = null; if (isset($replaces_map[$file_phid])) { $old_file_phid = $replaces_map[$file_phid]; $old_image = idx($mock_images, $old_file_phid); if ($old_image) { $replaces_image_phid = $old_image->getPHID(); } } $existing_image = idx($mock_images, $file_phid); $title = (string)$request->getStr('title_'.$file_phid); $description = (string)$request->getStr('description_'.$file_phid); $sequence = $sequence_map[$file_phid]; if ($replaces_image_phid) { $replace_image = id(new PholioImage()) ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransactionType::TYPE_IMAGE_REPLACE) ->setNewValue($replace_image); } else if (!$existing_image) { // this is an add $add_image = id(new PholioImage()) ->setFilePhid($file_phid) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransactionType::TYPE_IMAGE_FILE) ->setNewValue( array('+' => array($add_image))); } else { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransactionType::TYPE_IMAGE_NAME) ->setNewValue( array($existing_image->getPHID() => $title)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransactionType::TYPE_IMAGE_DESCRIPTION) ->setNewValue( array($existing_image->getPHID() => $description)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransactionType::TYPE_IMAGE_SEQUENCE) ->setNewValue( array($existing_image->getPHID() => $sequence)); } } foreach ($mock_images as $file_phid => $mock_image) { if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) { // this is an outright delete $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransactionType::TYPE_IMAGE_FILE) ->setNewValue( array('-' => array($mock_image))); } } $mock->openTransaction(); $editor = id(new PholioMockEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($user); $xactions = $editor->applyTransactions($mock, $xactions); $mock->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI('/M'.$mock->getID()); } } if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } else { $error_view = null; } if ($this->id) { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton('/M'.$this->id) ->setValue(pht('Save')); } else { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Create')); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($mock) ->execute(); // NOTE: Make this show up correctly on the rendered form. $mock->setViewPolicy($v_view); $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($v_cc) ->execute(); $image_elements = array(); foreach ($mock_images as $mock_image) { $image_elements[] = id(new PholioUploadedImageView()) ->setUser($user) ->setImage($mock_image); } $list_id = celerity_generate_unique_node_id(); $drop_id = celerity_generate_unique_node_id(); $order_id = celerity_generate_unique_node_id(); $list_control = phutil_tag( 'div', array( 'id' => $list_id, 'class' => 'pholio-edit-list', ), $image_elements); $drop_control = phutil_tag( 'div', array( 'id' => $drop_id, 'class' => 'pholio-edit-drop', ), 'Drag and drop images here to add them to the mock.'); $order_control = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'imageOrder', 'id' => $order_id, )); Javelin::initBehavior( 'pholio-mock-edit', array( 'listID' => $list_id, 'dropID' => $drop_id, 'orderID' => $order_id, 'uploadURI' => '/file/dropupload/', 'renderURI' => $this->getApplicationURI('image/upload/'), 'pht' => array( 'uploading' => pht('Uploading Image...'), 'uploaded' => pht('Upload Complete...'), 'undo' => pht('Undo'), 'removed' => pht('This image will be removed from the mock.'), ), )); require_celerity_resource('pholio-edit-css'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild($order_control) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setValue($v_name) ->setLabel(pht('Name')) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setValue($v_desc) ->setLabel(pht('Description')) ->setUser($user)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CC')) ->setName('cc') ->setValue($handles) ->setUser($user) ->setDatasource('/typeahead/common/mailable/')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($mock) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($list_control)) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($drop_control) ->setError($e_images)) ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($this->getApplicationURI())); + $crumbs->addTextCrumb($title, $this->getApplicationURI()); $content = array( $crumbs, $form_box, ); return $this->buildApplicationPage( $content, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index c4f786c12..e91376830 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -1,259 +1,256 @@ maniphestTaskPHIDs = $maniphest_task_phids; return $this; } private function getManiphestTaskPHIDs() { return $this->maniphestTaskPHIDs; } public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = $data['id']; $this->imageID = idx($data, 'imageID'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $mock = id(new PholioMockQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needImages(true) ->needCoverFiles(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $xactions = id(new PholioTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($mock->getPHID())) ->execute(); $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK); $this->setManiphestTaskPHIDs($phids); $phids[] = $mock->getAuthorPHID(); $this->loadHandles($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $title = $mock->getName(); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($user) ->setPolicyObject($mock); $actions = $this->buildActionView($mock); $properties = $this->buildPropertyView($mock, $engine, $actions); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); $image_status = $this->getImageStatus($mock, $this->imageID); $comment_form_id = celerity_generate_unique_node_id(); $output = id(new PholioMockImagesView()) ->setRequestURI($request->getRequestURI()) ->setCommentFormID($comment_form_id) ->setUser($user) ->setMock($mock) ->setImageID($this->imageID); $xaction_view = id(new PholioTransactionView()) ->setUser($this->getRequest()->getUser()) ->setObjectPHID($mock->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment = $this->buildAddCommentView($mock, $comment_form_id); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('M'.$mock->getID()) - ->setHref('/M'.$mock->getID())); + $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $content = array( $crumbs, $image_status, $object_box, $output->render(), $xaction_view, $add_comment, ); return $this->buildApplicationPage( $content, array( 'title' => 'M'.$mock->getID().' '.$title, 'device' => true, 'pageObjects' => array($mock->getPHID()), )); } private function getImageStatus(PholioMock $mock, $image_id) { $status = null; $images = $mock->getImages(); foreach ($images as $image) { if ($image->getID() == $image_id) { return $status; } } $images = $mock->getAllImages(); $images = mpull($images, null, 'getID'); $image = idx($images, $image_id); if ($image) { $history = $mock->getImageHistorySet($image_id); $latest_image = last($history); $href = $this->getApplicationURI( 'image/history/'.$latest_image->getID().'/'); $status = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('The requested image is obsolete.')) ->appendChild(phutil_tag( 'p', array(), array( pht('You are viewing this mock with the latest image set.'), ' ', phutil_tag( 'a', array('href' => $href), pht( 'Click here to see the history of the now obsolete image.'))))); } return $status; } private function buildActionView(PholioMock $mock) { $user = $this->getRequest()->getUser(); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($mock); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $mock, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Mock')) ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('attach') ->setName(pht('Edit Maniphest Tasks')) ->setHref("/search/attach/{$mock->getPHID()}/TASK/edge/") ->setDisabled(!$user->isLoggedIn()) ->setWorkflow(true)); return $actions; } private function buildPropertyView( PholioMock $mock, PhabricatorMarkupEngine $engine, PhabricatorActionListView $actions) { $user = $this->getRequest()->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setObject($mock) ->setActionList($actions); $properties->addProperty( pht('Author'), $this->getHandle($mock->getAuthorPHID())->renderLink()); $properties->addProperty( pht('Created'), phabricator_datetime($mock->getDateCreated(), $user)); if ($this->getManiphestTaskPHIDs()) { $properties->addProperty( pht('Maniphest Tasks'), $this->renderHandlesForPHIDs($this->getManiphestTaskPHIDs())); } $properties->invokeWillRenderEvent(); $properties->addImageContent( $engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION)); return $properties; } private function buildAddCommentView(PholioMock $mock, $comment_form_id) { $user = $this->getRequest()->getUser(); $draft = PhabricatorDraft::newFromUserAndKey($user, $mock->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('History Beckons'); $button_name = $is_serious ? pht('Add Comment') : pht('Answer The Call'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($mock->getPHID()) ->setFormID($comment_form_id) ->setDraft($draft) ->setHeaderText($title) ->setSubmitButtonName($button_name) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index a98a01317..d8875b937 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -1,183 +1,180 @@ accountID = $data['accountID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $account = id(new PhortuneAccountQuery()) ->setViewer($user) ->withIDs(array($this->accountID)) ->executeOne(); if (!$account) { return new Aphront404Response(); } $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Account')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Account'), $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Account')) ->setIcon('edit') ->setHref('#') ->setDisabled(true)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('transcript') ->setHref('#') ->setDisabled(true)); $crumbs->setActionList($actions); $properties = id(new PHUIPropertyListView()) ->setObject($account) ->setUser($user); $properties->addProperty(pht('Balance'), $account->getBalanceInCents()); $properties->setActionList($actions); $payment_methods = $this->buildPaymentMethodsSection($account); $purchase_history = $this->buildPurchaseHistorySection($account); $account_history = $this->buildAccountHistorySection($account); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $payment_methods, $purchase_history, $account_history, ), array( 'title' => $title, 'device' => true, )); } private function buildPaymentMethodsSection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Payment Methods')); $id = $account->getID(); $add_uri = $this->getApplicationURI($id.'/paymentmethod/edit/'); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Payment Method')) ->setIcon('new') ->setHref($add_uri)); $list = id(new PHUIObjectItemListView()) ->setUser($user) ->setNoDataString( pht('No payment methods associated with this account.')); $methods = id(new PhortunePaymentMethodQuery()) ->setViewer($user) ->withAccountPHIDs(array($account->getPHID())) ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) ->execute(); if ($methods) { $this->loadHandles(mpull($methods, 'getAuthorPHID')); } foreach ($methods as $method) { $item = new PHUIObjectItemView(); $item->setHeader($method->getBrand().' / '.$method->getLastFourDigits()); switch ($method->getStatus()) { case PhortunePaymentMethod::STATUS_ACTIVE: $item->addAttribute(pht('Active')); $item->setBarColor('green'); break; } $item->addAttribute( pht( 'Added %s by %s', phabricator_datetime($method->getDateCreated(), $user), $this->getHandle($method->getAuthorPHID())->renderLink())); $list->addItem($item); } return array( $header, $actions, $list, ); } private function buildPurchaseHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Purchase History')); return array( $header, ); } private function buildAccountHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Account History')); $xactions = id(new PhortuneAccountTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($account->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($account->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); return array( $header, $xaction_view, ); } } diff --git a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php index 28059f72f..4a82a219c 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodEditController.php @@ -1,240 +1,234 @@ accountID = $data['accountID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $account = id(new PhortuneAccountQuery()) ->setViewer($user) ->withIDs(array($this->accountID)) ->executeOne(); if (!$account) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI($account->getID().'/'); $account_uri = $this->getApplicationURI($account->getID().'/'); $providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); if (!$providers) { throw new Exception( "There are no payment providers enabled that can add payment ". "methods."); } $provider_key = $request->getStr('providerKey'); if (empty($providers[$provider_key])) { $choices = array(); foreach ($providers as $provider) { $choices[] = $this->renderSelectProvider($provider); } return $this->buildResponse($choices, $account_uri); } $provider = $providers[$provider_key]; $errors = array(); if ($request->isFormPost() && $request->getBool('isProviderForm')) { $method = id(new PhortunePaymentMethod()) ->setAccountPHID($account->getPHID()) ->setAuthorPHID($user->getPHID()) ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) ->setProviderType($provider->getProviderType()) ->setProviderDomain($provider->getProviderDomain()); if (!$errors) { $errors = $this->processClientErrors( $provider, $request->getStr('errors')); } if (!$errors) { $client_token_raw = $request->getStr('token'); $client_token = json_decode($client_token_raw, true); if (!is_array($client_token)) { $errors[] = pht( 'There was an error decoding token information submitted by the '. 'client. Expected a JSON-encoded token dictionary, received: %s.', nonempty($client_token_raw, pht('nothing'))); } else { if (!$provider->validateCreatePaymentMethodToken($client_token)) { $errors[] = pht( 'There was an error with the payment token submitted by the '. 'client. Expected a valid dictionary, received: %s.', $client_token_raw); } } if (!$errors) { $errors = $provider->createPaymentMethodFromRequest( $request, $method, $client_token); } } if (!$errors) { $method->save(); $save_uri = new PhutilURI($account_uri); $save_uri->setFragment('payment'); return id(new AphrontRedirectResponse())->setURI($save_uri); } else { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Error Adding Payment Method')) ->appendChild(id(new AphrontErrorView())->setErrors($errors)) ->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } $form = $provider->renderCreatePaymentMethodForm($request, $errors); $form ->setUser($user) ->setAction($request->getRequestURI()) ->setWorkflow(true) ->addHiddenInput('providerKey', $provider_key) ->addHiddenInput('isProviderForm', true) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Payment Method')) ->addCancelButton($account_uri)); if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } return $this->buildResponse( array($errors, $form), $account_uri); } private function renderSelectProvider( PhortunePaymentProvider $provider) { $request = $this->getRequest(); $user = $request->getUser(); $description = $provider->getPaymentMethodDescription(); $icon = $provider->getPaymentMethodIcon(); $details = $provider->getPaymentMethodProviderDescription(); $button = phutil_tag( 'button', array( 'class' => 'grey', ), array( $description, phutil_tag('br'), $icon, $details, )); $form = id(new AphrontFormView()) ->setUser($user) ->addHiddenInput('providerKey', $provider->getProviderKey()) ->appendChild($button); return $form; } private function buildResponse($content, $account_uri) { $request = $this->getRequest(); $title = pht('Add Payment Method'); $header = id(new PHUIHeaderView()) ->setHeader($title); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Account')) - ->setHref($account_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Payment Methods')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb(pht('Account'), $account_uri); + $crumbs->addTextCrumb(pht('Payment Methods'), $request->getRequestURI()); return $this->buildApplicationPage( array( $crumbs, $header, $content, ), array( 'title' => $title, 'device' => true, )); } private function processClientErrors( PhortunePaymentProvider $provider, $client_errors_raw) { $errors = array(); $client_errors = json_decode($client_errors_raw, true); if (!is_array($client_errors)) { $errors[] = pht( 'There was an error decoding error information submitted by the '. 'client. Expected a JSON-encoded list of error codes, received: %s.', nonempty($client_errors_raw, pht('nothing'))); } foreach (array_unique($client_errors) as $key => $client_error) { $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( $client_error); } foreach (array_unique($client_errors) as $client_error) { switch ($client_error) { case PhortuneErrCode::ERR_CC_INVALID_NUMBER: $message = pht( 'The card number you entered is not a valid card number. Check '. 'that you entered it correctly.'); break; case PhortuneErrCode::ERR_CC_INVALID_CVC: $message = pht( 'The CVC code you entered is not a valid CVC code. Check that '. 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. 'numeric code which usually appears on the back of the card.'); break; case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: $message = pht( 'The card expiration date is not a valid expiration date. Check '. 'that you entered it correctly. You can not add an expired card '. 'as a payment method.'); break; default: $message = $provider->getCreatePaymentErrorMessage($client_error); if (!$message) { $message = pht( "There was an unexpected error ('%s') processing payment ". "information.", $client_error); phlog($message); } break; } $errors[$client_error] = $message; } return $errors; } } diff --git a/src/applications/phortune/controller/PhortuneProductEditController.php b/src/applications/phortune/controller/PhortuneProductEditController.php index 0e93ce29e..f62817eca 100644 --- a/src/applications/phortune/controller/PhortuneProductEditController.php +++ b/src/applications/phortune/controller/PhortuneProductEditController.php @@ -1,163 +1,161 @@ productID = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->productID) { $product = id(new PhortuneProductQuery()) ->setViewer($user) ->withIDs(array($this->productID)) ->executeOne(); if (!$product) { return new Aphront404Response(); } $is_create = false; $cancel_uri = $this->getApplicationURI( 'product/view/'.$this->productID.'/'); } else { $product = new PhortuneProduct(); $is_create = true; $cancel_uri = $this->getApplicationURI('product/'); } $v_name = $product->getProductName(); $v_type = $product->getProductType(); $v_price = (int)$product->getPriceInCents(); $display_price = PhortuneCurrency::newFromUSDCents($v_price) ->formatForDisplay(); $e_name = true; $e_type = null; $e_price = true; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); if (!strlen($v_name)) { $e_name = pht('Required'); $errors[] = pht('Product must have a name.'); } else { $e_name = null; } if ($is_create) { $v_type = $request->getStr('type'); $type_map = PhortuneProduct::getTypeMap(); if (empty($type_map[$v_type])) { $e_type = pht('Invalid'); $errors[] = pht('Product type is invalid.'); } else { $e_type = null; } } $display_price = $request->getStr('price'); try { $v_price = PhortuneCurrency::newFromUserInput($user, $display_price) ->getValue(); $e_price = null; } catch (Exception $ex) { $errors[] = pht('Price should be formatted as: $1.23'); $e_price = pht('Invalid'); } if (!$errors) { $xactions = array(); $xactions[] = id(new PhortuneProductTransaction()) ->setTransactionType(PhortuneProductTransaction::TYPE_NAME) ->setNewValue($v_name); $xactions[] = id(new PhortuneProductTransaction()) ->setTransactionType(PhortuneProductTransaction::TYPE_TYPE) ->setNewValue($v_type); $xactions[] = id(new PhortuneProductTransaction()) ->setTransactionType(PhortuneProductTransaction::TYPE_PRICE) ->setNewValue($v_price); $editor = id(new PhortuneProductEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($product, $xactions); return id(new AphrontRedirectResponse())->setURI( $this->getApplicationURI('product/view/'.$product->getID().'/')); } } if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Type')) ->setName('type') ->setValue($v_type) ->setError($e_type) ->setOptions(PhortuneProduct::getTypeMap()) ->setDisabled(!$is_create)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Price')) ->setName('price') ->setValue($display_price) ->setError($e_price)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue( $is_create ? pht('Create Product') : pht('Save Product')) ->addCancelButton($cancel_uri)); $title = pht('Edit Product'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Products')) - ->setHref($this->getApplicationURI('product/'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($is_create ? pht('Create') : pht('Edit')) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb( + pht('Products'), + $this->getApplicationURI('product/')); + $crumbs->addTextCrumb( + $is_create ? pht('Create') : pht('Edit'), + $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Product')); return $this->buildApplicationPage( array( $crumbs, $header, $errors, $form, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phortune/controller/PhortuneProductListController.php b/src/applications/phortune/controller/PhortuneProductListController.php index fab506ff3..9a7c22bd1 100644 --- a/src/applications/phortune/controller/PhortuneProductListController.php +++ b/src/applications/phortune/controller/PhortuneProductListController.php @@ -1,67 +1,64 @@ getRequest(); $user = $request->getUser(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $query = id(new PhortuneProductQuery()) ->setViewer($user); $products = $query->executeWithCursorPager($pager); $title = pht('Product List'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Products') - ->setHref($this->getApplicationURI('product/'))); + $crumbs->addTextCrumb('Products', $this->getApplicationURI('product/')); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Product')) ->setHref($this->getApplicationURI('product/edit/')) ->setIcon('create')); $header = id(new PHUIHeaderView()) ->setHeader(pht('Product List')); $product_list = id(new PHUIObjectItemListView()) ->setUser($user) ->setNoDataString(pht('No products.')); foreach ($products as $product) { $view_uri = $this->getApplicationURI( 'product/view/'.$product->getID().'/'); $price = $product->getPriceInCents(); $item = id(new PHUIObjectItemView()) ->setObjectName($product->getID()) ->setHeader($product->getProductName()) ->setHref($view_uri) ->addAttribute( PhortuneCurrency::newFromUSDCents($price)->formatForDisplay()) ->addAttribute($product->getTypeName()); $product_list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $header, $product_list, $pager, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phortune/controller/PhortuneProductViewController.php b/src/applications/phortune/controller/PhortuneProductViewController.php index 29f8ccae9..f3c8fcf62 100644 --- a/src/applications/phortune/controller/PhortuneProductViewController.php +++ b/src/applications/phortune/controller/PhortuneProductViewController.php @@ -1,101 +1,99 @@ productID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $product = id(new PhortuneProductQuery()) ->setViewer($user) ->withIDs(array($this->productID)) ->executeOne(); if (!$product) { return new Aphront404Response(); } $title = pht('Product: %s', $product->getProductName()); $header = id(new PHUIHeaderView()) ->setHeader($product->getProductName()); $account = $this->loadActiveAccount($user); $edit_uri = $this->getApplicationURI('product/edit/'.$product->getID().'/'); $cart_uri = $this->getApplicationURI( $account->getID().'/buy/'.$product->getID().'/'); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Product')) ->setHref($edit_uri) ->setIcon('edit')) ->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setName(pht('Purchase')) ->setHref($cart_uri) ->setIcon('new') ->setRenderAsForm(true)); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Products')) - ->setHref($this->getApplicationURI('product/'))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('#%d', $product->getID())) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb( + pht('Products'), + $this->getApplicationURI('product/')); + $crumbs->addTextCrumb( + pht('#%d', $product->getID()), + $request->getRequestURI()); $properties = id(new PHUIPropertyListView()) ->setUser($user) ->setActionList($actions) ->addProperty(pht('Type'), $product->getTypeName()) ->addProperty( pht('Price'), PhortuneCurrency::newFromUSDCents($product->getPriceInCents()) ->formatForDisplay()); $xactions = id(new PhortuneProductTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($product->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($product->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $xaction_view, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/phragment/controller/PhragmentController.php b/src/applications/phragment/controller/PhragmentController.php index 200772377..912b729f7 100644 --- a/src/applications/phragment/controller/PhragmentController.php +++ b/src/applications/phragment/controller/PhragmentController.php @@ -1,237 +1,233 @@ setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withPaths($combinations) ->execute(); foreach ($combinations as $combination) { $found = false; foreach ($results as $fragment) { if ($fragment->getPath() === $combination) { $fragments[] = $fragment; $found = true; break; } } if (!$found) { return null; } } return $fragments; } protected function buildApplicationCrumbsWithPath(array $fragments) { $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('/') - ->setHref('/phragment/')); + $crumbs->addTextCrumb('/', '/phragment/'); foreach ($fragments as $parent) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($parent->getName()) - ->setHref('/phragment/browse/'.$parent->getPath())); + $crumbs->addTextCrumb( + $parent->getName(), + '/phragment/browse/'.$parent->getPath()); } return $crumbs; } protected function createCurrentFragmentView($fragment, $is_history_view) { if ($fragment === null) { return null; } $viewer = $this->getRequest()->getUser(); $phids = array(); $phids[] = $fragment->getLatestVersionPHID(); $snapshot_phids = array(); $snapshots = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) ->execute(); foreach ($snapshots as $snapshot) { $phids[] = $snapshot->getPHID(); $snapshot_phids[] = $snapshot->getPHID(); } $this->loadHandles($phids); $file = null; $file_uri = null; if (!$fragment->isDirectory()) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($fragment->getLatestVersion()->getFilePHID())) ->executeOne(); if ($file !== null) { $file_uri = $file->getDownloadURI(); } } $header = id(new PHUIHeaderView()) ->setHeader($fragment->getName()) ->setPolicyObject($fragment) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $fragment, PhabricatorPolicyCapability::CAN_EDIT); $zip_uri = $this->getApplicationURI("zip/".$fragment->getPath()); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($fragment) ->setObjectURI($fragment->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Fragment')) ->setHref($this->isCorrectlyConfigured() ? $file_uri : null) ->setDisabled($file === null || !$this->isCorrectlyConfigured()) ->setIcon('download')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Contents as ZIP')) ->setHref($this->isCorrectlyConfigured() ? $zip_uri : null) ->setDisabled(!$this->isCorrectlyConfigured()) ->setIcon('zip')); if (!$fragment->isDirectory()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Update Fragment')) ->setHref($this->getApplicationURI("update/".$fragment->getPath())) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setIcon('edit')); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Convert to File')) ->setHref($this->getApplicationURI("update/".$fragment->getPath())) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setIcon('edit')); } $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Set Fragment Policies')) ->setHref($this->getApplicationURI("policy/".$fragment->getPath())) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setIcon('edit')); if ($is_history_view) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('View Child Fragments')) ->setHref($this->getApplicationURI("browse/".$fragment->getPath())) ->setIcon('browse')); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($this->getApplicationURI("history/".$fragment->getPath())) ->setIcon('history')); } $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Snapshot')) ->setHref($this->getApplicationURI( "snapshot/create/".$fragment->getPath())) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setIcon('snapshot')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Promote Snapshot to Here')) ->setHref($this->getApplicationURI( "snapshot/promote/latest/".$fragment->getPath())) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('promote')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($fragment) ->setActionList($actions); if (!$fragment->isDirectory()) { if ($fragment->isDeleted()) { $properties->addProperty( pht('Type'), pht('File (Deleted)')); } else { $properties->addProperty( pht('Type'), pht('File')); } $properties->addProperty( pht('Latest Version'), $this->renderHandlesForPHIDs(array($fragment->getLatestVersionPHID()))); } else { $properties->addProperty( pht('Type'), pht('Directory')); } if (count($snapshot_phids) > 0) { $properties->addProperty( pht('Snapshots'), $this->renderHandlesForPHIDs($snapshot_phids)); } return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } function renderConfigurationWarningIfRequired() { $alt = PhabricatorEnv::getEnvConfig("security.alternate-file-domain"); if ($alt === null) { return id(new AphrontErrorView()) ->setTitle(pht('security.alternate-file-domain must be configured!')) ->setSeverity(AphrontErrorView::SEVERITY_ERROR) ->appendChild(phutil_tag('p', array(), pht( 'Because Phragment generates files (such as ZIP archives and '. 'patches) as they are requested, it requires that you configure '. 'the `security.alterate-file-domain` option. This option on it\'s '. 'own will also provide additional security when serving files '. 'across Phabricator.'))); } return null; } /** * We use this to disable the download links if the alternate domain is * not configured correctly. Although the download links will mostly work * for logged in users without an alternate domain, the behaviour is * reasonably non-consistent and will deny public users, even if policies * are configured otherwise (because the Files app does not support showing * the info page to viewers who are not logged in). */ function isCorrectlyConfigured() { $alt = PhabricatorEnv::getEnvConfig("security.alternate-file-domain"); return $alt !== null; } } diff --git a/src/applications/phragment/controller/PhragmentCreateController.php b/src/applications/phragment/controller/PhragmentCreateController.php index 3927fb203..564091337 100644 --- a/src/applications/phragment/controller/PhragmentCreateController.php +++ b/src/applications/phragment/controller/PhragmentCreateController.php @@ -1,133 +1,131 @@ dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parent = null; $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } if (count($parents) !== 0) { $parent = idx($parents, count($parents) - 1, null); } $parent_path = ''; if ($parent !== null) { $parent_path = $parent->getPath(); } $parent_path = trim($parent_path, '/'); $fragment = id(new PhragmentFragment()); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_name = $request->getStr('name'); $v_fileid = $request->getInt('fileID'); $v_viewpolicy = $request->getStr('viewPolicy'); $v_editpolicy = $request->getStr('editPolicy'); if (strpos($v_name, '/') !== false) { $errors[] = pht('The fragment name can not contain \'/\'.'); } $file = id(new PhabricatorFile())->load($v_fileid); if ($file === null) { $errors[] = pht('The specified file doesn\'t exist.'); } if (!count($errors)) { $depth = 1; if ($parent !== null) { $depth = $parent->getDepth() + 1; } PhragmentFragment::createFromFile( $viewer, $file, trim($parent_path.'/'.$v_name, '/'), $v_viewpolicy, $v_editpolicy); return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.trim($parent_path.'/'.$v_name, '/')); } else { $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('Errors while creating fragment')); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($fragment) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Parent Path')) ->setDisabled(true) ->setValue('/'.trim($parent_path.'/', '/'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('File ID')) ->setName('fileID')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('viewPolicy') ->setPolicyObject($fragment) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('editPolicy') ->setPolicyObject($fragment) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Fragment')) ->addCancelButton( $this->getApplicationURI('browse/'.$parent_path))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Fragment'))); + $crumbs->addTextCrumb(pht('Create Fragment')); $box = id(new PHUIObjectBoxView()) ->setHeaderText('Create Fragment') ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box), array( 'title' => pht('Create Fragment'), 'device' => true)); } } diff --git a/src/applications/phragment/controller/PhragmentPolicyController.php b/src/applications/phragment/controller/PhragmentPolicyController.php index e4ef33c21..b135e23f5 100644 --- a/src/applications/phragment/controller/PhragmentPolicyController.php +++ b/src/applications/phragment/controller/PhragmentPolicyController.php @@ -1,110 +1,108 @@ dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = idx($parents, count($parents) - 1, null); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); $v_replace_children = $request->getBool('replacePoliciesOnChildren'); $fragment->setViewPolicy($v_view_policy); $fragment->setEditPolicy($v_edit_policy); $fragment->save(); if ($v_replace_children) { // If you can edit a fragment, you can forcibly set the policies // on child fragments, regardless of whether you can see them or not. $children = id(new PhragmentFragmentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withLeadingPath($fragment->getPath().'/') ->execute(); $children_phids = mpull($children, 'getPHID'); $fragment->openTransaction(); foreach ($children as $child) { $child->setViewPolicy($v_view_policy); $child->setEditPolicy($v_edit_policy); $child->save(); } $fragment->saveTransaction(); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.$fragment->getPath()); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($fragment) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($fragment) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($fragment) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'replacePoliciesOnChildren', 'true', pht( 'Replace policies on child fragments with '. 'the policies above.'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Fragment Policies')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Fragment Policies'))); + $crumbs->addTextCrumb(pht('Edit Fragment Policies')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Fragment Policies: %s', $fragment->getPath())) ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box), array( 'title' => pht('Edit Fragment Policies'), 'device' => true)); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php index 923045ce5..48018cc57 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php @@ -1,176 +1,174 @@ dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = nonempty(last($parents), null); if ($fragment === null) { return new Aphront404Response(); } PhabricatorPolicyFilter::requireCapability( $viewer, $fragment, PhabricatorPolicyCapability::CAN_EDIT); $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($fragment->getPath().'/') ->execute(); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_name = $request->getStr('name'); if (strlen($v_name) === 0) { $errors[] = pht('You must specify a name.'); } if (strpos($v_name, '/') !== false) { $errors[] = pht('Snapshot names can not contain "/".'); } if (!count($errors)) { $snapshot = null; try { // Create the snapshot. $snapshot = id(new PhragmentSnapshot()) ->setPrimaryFragmentPHID($fragment->getPHID()) ->setName($v_name) ->save(); } catch (AphrontQueryDuplicateKeyException $e) { $errors[] = pht('A snapshot with this name already exists.'); } if (!count($errors)) { // Add the primary fragment. id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($fragment->getPHID()) ->setFragmentVersionPHID($fragment->getLatestVersionPHID()) ->save(); // Add all of the child fragments. foreach ($children as $child) { id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($child->getPHID()) ->setFragmentVersionPHID($child->getLatestVersionPHID()) ->save(); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/snapshot/view/'.$snapshot->getID()); } } $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('Errors while creating snapshot')); } $fragment_sequence = "-"; if ($fragment->getLatestVersion() !== null) { $fragment_sequence = $fragment->getLatestVersion()->getSequence(); } $rows = array(); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('th', array(), 'Fragment'), phutil_tag('th', array(), 'Version'))); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $fragment->getPath()), phutil_tag('td', array(), $fragment_sequence))); foreach ($children as $child) { $sequence = "-"; if ($child->getLatestVersion() !== null) { $sequence = $child->getLatestVersion()->getSequence(); } $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $child->getPath()), phutil_tag('td', array(), $sequence))); } $table = phutil_tag( 'table', array('class' => 'remarkup-table'), $rows); $container = phutil_tag( 'div', array('class' => 'phabricator-remarkup'), array( phutil_tag( 'p', array(), pht( "The snapshot will contain the following fragments at ". "the specified versions: ")), $table)); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Fragment Path')) ->setDisabled(true) ->setValue('/'.$fragment->getPath())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Snapshot Name')) ->setName('name')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Snapshot')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))) ->appendChild( id(new PHUIFormDividerControl())) ->appendInstructions($container); $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Snapshot'))); + $crumbs->addTextCrumb(pht('Create Snapshot')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Snapshot of %s', $fragment->getName())) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box), array( 'title' => pht('Create Fragment'), 'device' => true)); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotViewController.php b/src/applications/phragment/controller/PhragmentSnapshotViewController.php index 4cf0b577e..47663e633 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotViewController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotViewController.php @@ -1,156 +1,154 @@ id = idx($data, "id", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $snapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if ($snapshot === null) { return new Aphront404Response(); } $box = $this->createSnapshotView($snapshot); $fragment = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->withPHIDs(array($snapshot->getPrimaryFragmentPHID())) ->executeOne(); if ($fragment === null) { return new Aphront404Response(); } $parents = $this->loadParentFragments($fragment->getPath()); if ($parents === null) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('"%s" Snapshot', $snapshot->getName()))); + $crumbs->addTextCrumb(pht('"%s" Snapshot', $snapshot->getName())); $children = id(new PhragmentSnapshotChildQuery()) ->setViewer($viewer) ->needFragments(true) ->needFragmentVersions(true) ->withSnapshotPHIDs(array($snapshot->getPHID())) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($children as $child) { $item = id(new PHUIObjectItemView()) ->setHeader($child->getFragment()->getPath()); if ($child->getFragmentVersion() !== null) { $item ->setHref($child->getFragmentVersion()->getURI()) ->addAttribute(pht( 'Version %s', $child->getFragmentVersion()->getSequence())); } else { $item ->setHref($child->getFragment()->getURI()) ->addAttribute(pht('Directory')); } $list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box, $list), array( 'title' => pht('View Snapshot'), 'device' => true)); } protected function createSnapshotView($snapshot) { if ($snapshot === null) { return null; } $viewer = $this->getRequest()->getUser(); $phids = array(); $phids[] = $snapshot->getPrimaryFragmentPHID(); $this->loadHandles($phids); $header = id(new PHUIHeaderView()) ->setHeader(pht('"%s" Snapshot', $snapshot->getName())) ->setPolicyObject($snapshot) ->setUser($viewer); $zip_uri = $this->getApplicationURI( "zip@".$snapshot->getName(). "/".$snapshot->getPrimaryFragment()->getPath()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $snapshot, PhabricatorPolicyCapability::CAN_EDIT); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($snapshot) ->setObjectURI($snapshot->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Snapshot as ZIP')) ->setHref($this->isCorrectlyConfigured() ? $zip_uri : null) ->setDisabled(!$this->isCorrectlyConfigured()) ->setIcon('zip')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Snapshot')) ->setHref($this->getApplicationURI( "snapshot/delete/".$snapshot->getID()."/")) ->setDisabled(!$can_edit) ->setWorkflow(true) ->setIcon('delete')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Promote Another Snapshot to Here')) ->setHref($this->getApplicationURI( "snapshot/promote/".$snapshot->getID()."/")) ->setDisabled(!$can_edit) ->setWorkflow(true) ->setIcon('promote')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($snapshot) ->setActionList($actions); $properties->addProperty( pht('Name'), $snapshot->getName()); $properties->addProperty( pht('Fragment'), $this->renderHandlesForPHIDs(array($snapshot->getPrimaryFragmentPHID()))); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } } diff --git a/src/applications/phragment/controller/PhragmentUpdateController.php b/src/applications/phragment/controller/PhragmentUpdateController.php index 1c233bb74..f14d16e2f 100644 --- a/src/applications/phragment/controller/PhragmentUpdateController.php +++ b/src/applications/phragment/controller/PhragmentUpdateController.php @@ -1,84 +1,82 @@ dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = idx($parents, count($parents) - 1, null); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_fileid = $request->getInt('fileID'); $file = id(new PhabricatorFile())->load($v_fileid); if ($file === null) { $errors[] = pht('The specified file doesn\'t exist.'); } if (!count($errors)) { // If the file is a ZIP archive (has application/zip mimetype) // then we extract the zip and apply versions for each of the // individual fragments, creating and deleting files as needed. if ($file->getMimeType() === "application/zip") { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.$fragment->getPath()); } else { $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('Errors while updating fragment')); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('File ID')) ->setName('fileID')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Fragment')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Update Fragment'))); + $crumbs->addTextCrumb(pht('Update Fragment')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Update Fragment: %s', $fragment->getPath())) ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box), array( 'title' => pht('Update Fragment'), 'device' => true)); } } diff --git a/src/applications/phragment/controller/PhragmentVersionController.php b/src/applications/phragment/controller/PhragmentVersionController.php index b5f144cc3..803d0afb5 100644 --- a/src/applications/phragment/controller/PhragmentVersionController.php +++ b/src/applications/phragment/controller/PhragmentVersionController.php @@ -1,138 +1,136 @@ id = idx($data, "id", 0); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $version = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if ($version === null) { return new Aphront404Response(); } $parents = $this->loadParentFragments($version->getFragment()->getPath()); if ($parents === null) { return new Aphront404Response(); } $current = idx($parents, count($parents) - 1, null); $crumbs = $this->buildApplicationCrumbsWithPath($parents); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('View Version %d', $version->getSequence()))); + $crumbs->addTextCrumb(pht('View Version %d', $version->getSequence())); $phids = array(); $phids[] = $version->getFilePHID(); $this->loadHandles($phids); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($version->getFilePHID())) ->executeOne(); if ($file !== null) { $file_uri = $file->getDownloadURI(); } $header = id(new PHUIHeaderView()) ->setHeader(pht( "%s at version %d", $version->getFragment()->getName(), $version->getSequence())) ->setPolicyObject($version) ->setUser($viewer); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($version) ->setObjectURI($version->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Version')) ->setDisabled($file === null || !$this->isCorrectlyConfigured()) ->setHref($this->isCorrectlyConfigured() ? $file_uri : null) ->setIcon('download')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($version) ->setActionList($actions); $properties->addProperty( pht('File'), $this->renderHandlesForPHIDs(array($version->getFilePHID()))); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box, $this->renderPreviousVersionList($version)), array( 'title' => pht('View Version'), 'device' => true)); } private function renderPreviousVersionList( PhragmentFragmentVersion $version) { $request = $this->getRequest(); $viewer = $request->getUser(); $previous_versions = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($version->getFragmentPHID())) ->withSequenceBefore($version->getSequence()) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($previous_versions as $previous_version) { $item = id(new PHUIObjectItemView()); $item->setHeader('Version '.$previous_version->getSequence()); $item->setHref($previous_version->getURI()); $item->addAttribute(phabricator_datetime( $previous_version->getDateCreated(), $viewer)); $patch_uri = $this->getApplicationURI( 'patch/'.$previous_version->getID().'/'.$version->getID()); $item->addAction(id(new PHUIListItemView()) ->setIcon('patch') ->setName(pht("Get Patch")) ->setHref($this->isCorrectlyConfigured() ? $patch_uri : null) ->setDisabled(!$this->isCorrectlyConfigured())); $list->addItem($item); } $item = id(new PHUIObjectItemView()); $item->setHeader('Prior to Version 0'); $item->addAttribute('Prior to any content (empty file)'); $item->addAction(id(new PHUIListItemView()) ->setIcon('patch') ->setName(pht("Get Patch")) ->setHref($this->getApplicationURI( 'patch/x/'.$version->getID()))); $list->addItem($item); return $list; } } diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php index 3e926db60..70e49a229 100644 --- a/src/applications/phriction/controller/PhrictionDiffController.php +++ b/src/applications/phriction/controller/PhrictionDiffController.php @@ -1,296 +1,291 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $document = id(new PhrictionDocument())->load($this->id); if (!$document) { return new Aphront404Response(); } $current = id(new PhrictionContent())->load($document->getContentID()); $l = $request->getInt('l'); $r = $request->getInt('r'); $ref = $request->getStr('ref'); if ($ref) { list($l, $r) = explode(',', $ref); } $content = id(new PhrictionContent())->loadAllWhere( 'documentID = %d AND version IN (%Ld)', $document->getID(), array($l, $r)); $content = mpull($content, null, 'getVersion'); $content_l = idx($content, $l, null); $content_r = idx($content, $r, null); if (!$content_l || !$content_r) { return new Aphront404Response(); } $text_l = $content_l->getContent(); $text_r = $content_r->getContent(); $text_l = phutil_utf8_hard_wrap($text_l, 80); $text_l = implode("\n", $text_l); $text_r = phutil_utf8_hard_wrap($text_r, 80); $text_r = implode("\n", $text_r); $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($text_l, $text_r); $changeset->setOldProperties( array( 'Title' => $content_l->getTitle(), )); $changeset->setNewProperties( array( 'Title' => $content_r->getTitle(), )); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $parser = new DifferentialChangesetParser(); $parser->setChangeset($changeset); $parser->setRenderingReference("{$l},{$r}"); $parser->setWhitespaceMode($whitespace_mode); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->process(); $parser->setMarkupEngine($engine); $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); if ($request->isAjax()) { return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($output); } require_celerity_resource('differential-changeset-view-css'); require_celerity_resource('syntax-highlighting-css'); require_celerity_resource('phriction-document-css'); Javelin::initBehavior('differential-show-more', array( 'uri' => '/phriction/diff/'.$document->getID().'/', 'whitespace' => $whitespace_mode, )); $slug = $document->getSlug(); $revert_l = $this->renderRevertButton($content_l, $current); $revert_r = $this->renderRevertButton($content_r, $current); $crumbs = $this->buildApplicationCrumbs(); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History')) - ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); - + $crumbs->addTextCrumb( + pht('History'), + PhrictionDocument::getSlugURI($slug, 'history')); $title = pht("Version %s vs %s", $l, $r); $header = id(new PHUIHeaderView()) ->setHeader($title); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title) - ->setHref($request->getRequestURI())); + $crumbs->addTextCrumb($title, $request->getRequestURI()); $comparison_table = $this->renderComparisonTable( array( $content_r, $content_l, )); $navigation_table = null; if ($l + 1 == $r) { $nav_l = ($l > 1); $nav_r = ($r != $current->getVersion()); $uri = $request->getRequestURI(); if ($nav_l) { $link_l = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1), 'class' => 'button', ), pht("\xC2\xAB Previous Change")); } else { $link_l = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button grey disabled', ), pht('Original Change')); } $link_r = null; if ($nav_r) { $link_r = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1), 'class' => 'button', ), pht("Next Change \xC2\xBB")); } else { $link_r = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button grey disabled', ), pht('Most Recent Change')); } $navigation_table = phutil_tag( 'table', array('class' => 'phriction-history-nav-table'), phutil_tag('tr', array(), array( phutil_tag('td', array('class' => 'nav-prev'), $link_l), phutil_tag('td', array('class' => 'nav-next'), $link_r), ))); } $output = hsprintf( '
    '. '%s%s'. ''. ''. '
    %s%s
    '. '%s'. '
    ', $comparison_table->render(), $navigation_table, $revert_l, $revert_r, $output); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($output); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => pht('Document History'), 'device' => true, )); } private function renderRevertButton( PhrictionContent $content, PhrictionContent $current) { $document_id = $content->getDocumentID(); $version = $content->getVersion(); $hidden_statuses = array( PhrictionChangeType::CHANGE_DELETE => true, // Silly PhrictionChangeType::CHANGE_MOVE_AWAY => true, // Plain silly PhrictionChangeType::CHANGE_STUB => true, // Utterly silly ); if (isset($hidden_statuses[$content->getChangeType()])) { // Don't show an edit/revert button for changes which deleted, moved or // stubbed the content since it's silly. return null; } if ($content->getID() == $current->getID()) { return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/', 'class' => 'button grey', ), pht('Edit Current Version')); } return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/?revert='.$version, 'class' => 'button grey', ), pht('Revert to Version %s...', $version)); } private function renderComparisonTable(array $content) { assert_instances_of($content, 'PhrictionContent'); $user = $this->getRequest()->getUser(); $phids = mpull($content, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $list = new PHUIObjectItemListView(); $list->setFlush(true); $first = true; foreach ($content as $c) { $author = $handles[$c->getAuthorPHID()]->renderLink(); $item = id(new PHUIObjectItemView()) ->setHeader(pht('%s by %s, %s', PhrictionChangeType::getChangeTypeLabel($c->getChangeType()), $author, pht('Version %s', $c->getVersion()))) ->addAttribute(pht('%s %s', phabricator_date($c->getDateCreated(), $user), phabricator_time($c->getDateCreated(), $user))); if ($c->getDescription()) { $item->addAttribute($c->getDescription()); } if ($first == true) { $item->setBarColor('green'); $first = false; } else { $item->setBarColor('red'); } $list->addItem($item); } return $list; } } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index fabaaea05..63f184dde 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -1,270 +1,265 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $document = id(new PhrictionDocument())->load($this->id); if (!$document) { return new Aphront404Response(); } $revert = $request->getInt('revert'); if ($revert) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $revert); if (!$content) { return new Aphront404Response(); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } } else { $slug = $request->getStr('slug'); $slug = PhabricatorSlug::normalize($slug); if (!$slug) { return new Aphront404Response(); } $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if (!$project) { return new Aphront404Response(); } } $document = new PhrictionDocument(); $document->setSlug($slug); $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); } } if ($request->getBool('nodraft')) { $draft = null; $draft_key = null; } else { if ($document->getPHID()) { $draft_key = $document->getPHID().':'.$content->getVersion(); } else { $draft_key = 'phriction:'.$content->getSlug(); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $draft_key); } require_celerity_resource('phriction-document-css'); $e_title = true; $notes = null; $errors = array(); if ($request->isFormPost()) { $title = $request->getStr('title'); $notes = $request->getStr('description'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('Document title is required.'); } else { $e_title = null; } if ($document->getID()) { if ($content->getTitle() == $title && $content->getContent() == $request->getStr('content')) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('No Edits')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You did not make any changes to the document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } else if (!strlen($request->getStr('content'))) { // We trigger this only for new pages. For existing pages, deleting // all the content counts as deleting the page. $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('Empty Page')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You can not create an empty document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!count($errors)) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) ->setActor($user) ->setTitle($title) ->setContent($request->getStr('content')) ->setDescription($notes); $editor->save(); if ($draft) { $draft->delete(); } $uri = PhrictionDocument::getSlugURI($document->getSlug()); return id(new AphrontRedirectResponse())->setURI($uri); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } if ($document->getID()) { $panel_header = pht('Edit Phriction Document'); $submit_button = pht('Save Changes'); } else { $panel_header = pht('Create New Phriction Document'); $submit_button = pht('Create Document'); } $uri = $document->getSlug(); $uri = PhrictionDocument::getSlugURI($uri); $uri = PhabricatorEnv::getProductionURI($uri); $cancel_uri = PhrictionDocument::getSlugURI($document->getSlug()); if ($draft && strlen($draft->getDraft()) && ($draft->getDraft() != $content->getContent())) { $content_text = $draft->getDraft(); $discard = phutil_tag( 'a', array( 'href' => $request->getRequestURI()->alter('nodraft', true), ), pht('discard this draft')); $draft_note = new AphrontErrorView(); $draft_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $draft_note->setTitle('Recovered Draft'); $draft_note->appendChild(hsprintf( '

    Showing a saved draft of your edits, you can %s.

    ', $discard)); } else { $content_text = $content->getContent(); $draft_note = null; } $form = id(new AphrontFormView()) ->setUser($user) ->setWorkflow(true) ->setAction($request->getRequestURI()->getPath()) ->addHiddenInput('slug', $document->getSlug()) ->addHiddenInput('nodraft', $request->getBool('nodraft')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($content->getTitle()) ->setError($e_title) ->setName('title')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('URI')) ->setValue($uri)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Content')) ->setValue($content_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('content') ->setID('document-textarea') ->setUser($user)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Edit Notes')) ->setValue($notes) ->setError(null) ->setName('description')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Document')) ->setFormError($error_view) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI('/phriction/preview/') ->setControlID('document-textarea') ->setSkin('document'); $crumbs = $this->buildApplicationCrumbs(); if ($document->getID()) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($content->getTitle()) - ->setHref(PhrictionDocument::getSlugURI($document->getSlug()))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb( + $content->getTitle(), + PhrictionDocument::getSlugURI($document->getSlug())); + $crumbs->addTextCrumb(pht('Edit')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create'))); + $crumbs->addTextCrumb(pht('Create')); } return $this->buildApplicationPage( array( $crumbs, $draft_note, $form_box, $preview, ), array( 'title' => pht('Edit Document'), 'device' => true, )); } } diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php index 53add915d..d44d8f8f4 100644 --- a/src/applications/phriction/controller/PhrictionHistoryController.php +++ b/src/applications/phriction/controller/PhrictionHistoryController.php @@ -1,171 +1,169 @@ slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', PhabricatorSlug::normalize($this->slug)); if (!$document) { return new Aphront404Response(); } $current = id(new PhrictionContent())->load($document->getContentID()); $pager = new AphrontPagerView(); $pager->setOffset($request->getInt('page')); $pager->setURI($request->getRequestURI(), 'page'); $history = id(new PhrictionContent())->loadAllWhere( 'documentID = %d ORDER BY version DESC LIMIT %d, %d', $document->getID(), $pager->getOffset(), $pager->getPageSize() + 1); $history = $pager->sliceResults($history); $author_phids = mpull($history, 'getAuthorPHID'); $handles = $this->loadViewerHandles($author_phids); $list = new PHUIObjectItemListView(); foreach ($history as $content) { $author = $handles[$content->getAuthorPHID()]->renderLink(); $slug_uri = PhrictionDocument::getSlugURI($document->getSlug()); $version = $content->getVersion(); $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/'); $vs_previous = null; if ($content->getVersion() != 1) { $vs_previous = $diff_uri ->alter('l', $content->getVersion() - 1) ->alter('r', $content->getVersion()); } $vs_head = null; if ($content->getID() != $document->getContentID()) { $vs_head = $diff_uri ->alter('l', $content->getVersion()) ->alter('r', $current->getVersion()); } $change_type = PhrictionChangeType::getChangeTypeLabel( $content->getChangeType()); switch ($content->getChangeType()) { case PhrictionChangeType::CHANGE_DELETE: $color = 'red'; break; case PhrictionChangeType::CHANGE_EDIT: $color = 'blue'; break; case PhrictionChangeType::CHANGE_MOVE_HERE: $color = 'yellow'; break; case PhrictionChangeType::CHANGE_MOVE_AWAY: $color = 'orange'; break; case PhrictionChangeType::CHANGE_STUB: $color = 'green'; break; default: throw new Exception("Unknown change type!"); break; } $item = id(new PHUIObjectItemView()) ->setHeader(pht('%s by %s', $change_type, $author)) ->setBarColor($color) ->addAttribute( phutil_tag( 'a', array( 'href' => $slug_uri.'?v='.$version, ), pht('Version %s', $version))) ->addAttribute(pht('%s %s', phabricator_date($content->getDateCreated(), $user), phabricator_time($content->getDateCreated(), $user))); if ($content->getDescription()) { $item->addAttribute($content->getDescription()); } if ($vs_previous) { $item->addIcon( 'arrow_left', pht('Show Change'), array( 'href' => $vs_previous, )); } else { $item->addIcon('arrow_left-grey', phutil_tag('em', array(), pht('No previous change'))); } if ($vs_head) { $item->addIcon( 'merge', pht('Show Later Changes'), array( 'href' => $vs_head, )); } else { $item->addIcon('merge-grey', phutil_tag('em', array(), pht('No later changes'))); } $list->addItem($item); } $crumbs = $this->buildApplicationCrumbs(); $crumb_views = $this->renderBreadcrumbs($document->getSlug()); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History')) - ->setHref( - PhrictionDocument::getSlugURI($document->getSlug(), 'history'))); + $crumbs->addTextCrumb( + pht('History'), + PhrictionDocument::getSlugURI($document->getSlug(), 'history')); $header = new PHUIHeaderView(); $header->setHeader(pht('Document History for %s', phutil_tag( 'a', array('href' => PhrictionDocument::getSlugURI($document->getSlug())), head($history)->getTitle()))); $obj_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($list) ->appendChild($pager); return $this->buildApplicationPage( array( $crumbs, $obj_box, ), array( 'title' => pht('Document History'), 'device' => true, )); } } diff --git a/src/applications/ponder/controller/PonderAnswerEditController.php b/src/applications/ponder/controller/PonderAnswerEditController.php index 75db3a1c2..4ef6f6fb5 100644 --- a/src/applications/ponder/controller/PonderAnswerEditController.php +++ b/src/applications/ponder/controller/PonderAnswerEditController.php @@ -1,119 +1,114 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $answer = id(new PonderAnswerQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$answer) { return new Aphront404Response(); } $v_content = $answer->getContent(); $e_content = true; $question = $answer->getQuestion(); $qid = $question->getID(); $answer_uri = $answer->getURI(); $errors = array(); if ($request->isFormPost()) { $v_content = $request->getStr('content'); if (!strlen($v_content)) { $errors[] = pht('You must provide some substance in your answer.'); $e_content = pht('Required'); } if (!$errors) { $xactions = array(); $xactions[] = id(new PonderAnswerTransaction()) ->setTransactionType(PonderAnswerTransaction::TYPE_CONTENT) ->setNewValue($v_content); $editor = id(new PonderAnswerEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $editor->applyTransactions($answer, $xactions); return id(new AphrontRedirectResponse()) ->setURI($answer_uri); } } if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $answer_content_id = celerity_generate_unique_node_id(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Question')) ->setValue($question->getTitle())) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Answer')) ->setName('content') ->setID($answer_content_id) ->setValue($v_content) ->setError($e_content)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Answer')) ->addCancelButton($answer_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref($answer_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Answer'))); + $crumbs->addTextCrumb("Q{$qid}", $answer_uri); + $crumbs->addTextCrumb(pht('Edit Answer')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Answer')) ->setFormError($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Answer Preview')) ->setControlID($answer_content_id) ->setPreviewURI($this->getApplicationURI('preview/')); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => pht('Edit Answer'), 'device' => true, )); } } diff --git a/src/applications/ponder/controller/PonderAnswerHistoryController.php b/src/applications/ponder/controller/PonderAnswerHistoryController.php index 324809ec2..b15e22888 100644 --- a/src/applications/ponder/controller/PonderAnswerHistoryController.php +++ b/src/applications/ponder/controller/PonderAnswerHistoryController.php @@ -1,72 +1,64 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $answer = id(new PonderAnswerQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$answer) { return new Aphront404Response(); } $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($answer->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $qid = $answer->getQuestion()->getID(); $aid = $answer->getID(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref("/Q{$qid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("A{$aid}") - ->setHref("/Q{$qid}#{$aid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); + $crumbs->addTextCrumb("A{$aid}", "/Q{$qid}#{$aid}"); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Answer History'), 'device' => true, )); } } diff --git a/src/applications/ponder/controller/PonderQuestionEditController.php b/src/applications/ponder/controller/PonderQuestionEditController.php index bcfc722ee..cfaa9a0a0 100644 --- a/src/applications/ponder/controller/PonderQuestionEditController.php +++ b/src/applications/ponder/controller/PonderQuestionEditController.php @@ -1,145 +1,138 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $question = id(new PonderQuestionQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$question) { return new Aphront404Response(); } } else { $question = id(new PonderQuestion()) ->setStatus(PonderQuestionStatus::STATUS_OPEN) ->setAuthorPHID($user->getPHID()) ->setVoteCount(0) ->setAnswerCount(0) ->setHeat(0.0); } $v_title = $question->getTitle(); $v_content = $question->getContent(); $errors = array(); $e_title = true; if ($request->isFormPost()) { $v_title = $request->getStr('title'); $v_content = $request->getStr('content'); $len = phutil_utf8_strlen($v_title); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } if (!$errors) { $template = id(new PonderQuestionTransaction()); $xactions = array(); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_TITLE) ->setNewValue($v_title); $xactions[] = id(clone $template) ->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT) ->setNewValue($v_content); $editor = id(new PonderQuestionEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); $editor->applyTransactions($question, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/Q'.$question->getID()); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Form Errors')) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Question')) ->setName('title') ->setValue($v_title) ->setError($e_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('content') ->setID('content') ->setValue($v_content) ->setLabel(pht('Description')) ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Ask Away!'))); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Question Preview')) ->setControlID('content') ->setPreviewURI($this->getApplicationURI('preview/')); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Ask New Question')) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $id = $question->getID(); if ($id) { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$id}") - ->setHref("/Q{$id}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb("Q{$id}", "/Q{$id}"); + $crumbs->addTextCrumb(pht('Edit')); } else { - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Ask Question'))); + $crumbs->addTextCrumb(pht('Ask Question')); } return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => pht('Ask New Question'), 'device' => true, )); } } diff --git a/src/applications/ponder/controller/PonderQuestionHistoryController.php b/src/applications/ponder/controller/PonderQuestionHistoryController.php index 4c8c382f8..09658c14e 100644 --- a/src/applications/ponder/controller/PonderQuestionHistoryController.php +++ b/src/applications/ponder/controller/PonderQuestionHistoryController.php @@ -1,67 +1,62 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $question = id(new PonderQuestionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$question) { return new Aphront404Response(); } $xactions = id(new PonderQuestionTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($question->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $qid = $question->getID(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName("Q{$qid}") - ->setHref("/Q{$qid}")); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb("Q{$qid}", "/Q{$qid}"); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Question History'), 'device' => true, )); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 3789d7a69..21f6d405a 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,417 +1,414 @@ questionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $question = id(new PonderQuestionQuery()) ->setViewer($user) ->withIDs(array($this->questionID)) ->needAnswers(true) ->needViewerVotes(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $question->attachVotes($user->getPHID()); $question_xactions = $this->buildQuestionTransactions($question); $answers = $this->buildAnswers($question->getAnswers()); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); if (isset($authors[$user->getPHID()])) { $answer_add_panel = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NODATA) ->appendChild( pht( 'You have already answered this question. You can not answer '. 'twice, but you can edit your existing answer.')); } else { $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) ->setUser($user) ->setActionURI("/ponder/answer/add/"); } $header = id(new PHUIHeaderView()) ->setHeader($question->getTitle()); $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->setActionList($actions); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('Q'.$this->questionID) - ->setHref('/Q'.$this->questionID)); + $crumbs->addTextCrumb('Q'.$this->questionID, '/Q'.$this->questionID); return $this->buildApplicationPage( array( $crumbs, $object_box, $question_xactions, $answers, $answer_add_panel ), array( 'device' => true, 'title' => 'Q'.$question->getID().' '.$question->getTitle(), 'pageObjects' => array($question->getPHID()), )); } private function buildActionListView(PonderQuestion $question) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($question) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht("Close Question"); $icon = "delete"; $href = "close"; } else { $name = pht("Reopen Question"); $icon = "enable"; $href = "open"; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setRenderAsForm($can_edit) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/{$href}/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setIcon('transcript') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $view; } private function buildPropertyListView( PonderQuestion $question, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) ->setActionList($actions); $this->loadHandles(array($question->getAuthorPHID())); $view->addProperty( pht('Status'), PonderQuestionStatus::getQuestionStatusFullName($question->getStatus())); $view->addProperty( pht('Author'), $this->getHandle($question->getAuthorPHID())->renderLink()); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($question->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($question->getVoteCount()) ->setVote($question->getUserVote()); $view->addSectionHeader(pht('Question')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer)), )); return $view; } private function buildQuestionTransactions(PonderQuestion $question) { $viewer = $this->getRequest()->getUser(); $id = $question->getID(); $xactions = id(new PonderQuestionTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(array($question->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); return $this->wrapComments( count($xactions), array( $timeline, $add_comment, )); } private function buildAnswers(array $answers) { $request = $this->getRequest(); $viewer = $request->getUser(); $out = array(); $phids = mpull($answers, 'getAuthorPHID'); $this->loadHandles($phids); $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(mpull($answers, 'getPHID')) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $xaction_groups = mgroup($xactions, 'getObjectPHID'); foreach ($answers as $answer) { $author_phid = $answer->getAuthorPHID(); $xactions = idx($xaction_groups, $answer->getPHID(), array()); $id = $answer->getID(); $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); $out[] = id(new PhabricatorAnchorView()) ->setAnchorName("A$id"); $header = id(new PHUIHeaderView()) ->setHeader($this->getHandle($author_phid)->getFullName()); $actions = $this->buildAnswerActions($answer); $properties = $this->buildAnswerProperties($answer, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $out[] = $object_box; $details = array(); $details[] = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Answer Comment')) ->setAction($this->getApplicationURI("/answer/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $details[] = $form; $out[] = $this->wrapComments( count($xactions), $details); } $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); return $out; } private function buildAnswerActions(PonderAnswer $answer) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $answer->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $answer, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($answer) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('edit') ->setName(pht('Edit Answer')) ->setHref($this->getApplicationURI("/answer/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setIcon('transcript') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/answer/history/{$id}/"))); return $view; } private function buildAnswerProperties( PonderAnswer $answer, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($answer) ->setActionList($actions); $view->addProperty( pht('Created'), phabricator_datetime($answer->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($answer->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($answer->getVoteCount()) ->setVote($answer->getUserVote()); $view->addSectionHeader(pht('Answer')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, $answer->getMarkupField(), $viewer)), )); return $view; } private function wrapComments($n, $stuff) { if ($n == 0) { $text = pht('Add a Comment'); } else { $text = pht('Show %s Comments', new PhutilNumber($n)); } $show_id = celerity_generate_unique_node_id(); $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); require_celerity_resource('ponder-comment-table-css'); $show = phutil_tag( 'div', array( 'id' => $show_id, 'class' => 'ponder-show-comments', ), javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => array($hide_id), 'hideIDs' => array($show_id), ), ), $text)); $hide = phutil_tag( 'div', array( 'id' => $hide_id, 'style' => 'display: none', ), $stuff); return array($show, $hide); } } diff --git a/src/applications/project/controller/PhabricatorProjectCreateController.php b/src/applications/project/controller/PhabricatorProjectCreateController.php index 76de5b83d..cdcfdb283 100644 --- a/src/applications/project/controller/PhabricatorProjectCreateController.php +++ b/src/applications/project/controller/PhabricatorProjectCreateController.php @@ -1,136 +1,135 @@ getRequest(); $user = $request->getUser(); $this->requireApplicationCapability( ProjectCapabilityCreateProjects::CAPABILITY); $project = new PhabricatorProject(); $project->setAuthorPHID($user->getPHID()); $project->attachMemberPHIDs(array()); $profile = new PhabricatorProjectProfile(); $e_name = true; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransaction::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransaction::TYPE_MEMBERS); $xaction->setNewValue(array($user->getPHID())); $xactions[] = $xaction; $editor = new PhabricatorProjectEditor($project); $editor->setActor($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent(array( 'phid' => $project->getPHID(), 'name' => $project->getName(), )); } else { return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } if ($request->isAjax()) { $form = new PHUIFormLayoutView(); } else { $form = new AphrontFormView(); $form->setUser($user); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Blurb')) ->setName('blurb') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($profile->getBlurb())); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Create a New Project')) ->appendChild($error_view) ->appendChild($form) ->addSubmitButton(pht('Create Project')) ->addCancelButton('/project/'); return id(new AphrontDialogResponse())->setDialog($dialog); } else { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create')) ->addCancelButton('/project/')); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Create Project')) - ->setHref($this->getApplicationURI().'create/')); + $crumbs->addTextCrumb( + pht('Create Project'), + $this->getApplicationURI().'create/'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Project')) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create New Project'), 'device' => true, )); } } } diff --git a/src/applications/project/controller/PhabricatorProjectHistoryController.php b/src/applications/project/controller/PhabricatorProjectHistoryController.php index d42c05afe..cd92ed338 100644 --- a/src/applications/project/controller/PhabricatorProjectHistoryController.php +++ b/src/applications/project/controller/PhabricatorProjectHistoryController.php @@ -1,60 +1,57 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $xactions = id(new PhabricatorProjectTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($project->getPHID())) ->execute(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($project->getPHID()) ->setTransactions($xactions); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref($this->getApplicationURI("view/{$id}/"))); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb( + $project->getName(), + $this->getApplicationURI("view/{$id}/")) + ->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => $project->getName(), 'device' => true, )); } } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php index fa81c442e..129f53ded 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -1,172 +1,168 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needMembers(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $member_phids = $project->getMemberPHIDs(); $errors = array(); if ($request->isFormPost()) { $changed_something = false; $member_map = array_fill_keys($member_phids, true); $remove = $request->getStr('remove'); if ($remove) { if (isset($member_map[$remove])) { unset($member_map[$remove]); $changed_something = true; } } else { $new_members = $request->getArr('phids'); foreach ($new_members as $member) { if (empty($member_map[$member])) { $member_map[$member] = true; $changed_something = true; } } } $xactions = array(); if ($changed_something) { $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransaction::TYPE_MEMBERS); $xaction->setNewValue(array_keys($member_map)); $xactions[] = $xaction; } if ($xactions) { $editor = new PhabricatorProjectEditor($project); $editor->setActor($user); $editor->applyTransactions($xactions); } return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()); } $member_phids = array_reverse($member_phids); $handles = $this->loadViewerHandles($member_phids); $state = array(); foreach ($handles as $handle) { $state[] = array( 'phid' => $handle->getPHID(), 'name' => $handle->getFullName(), ); } $header_name = pht('Edit Members'); $title = pht('Edit Members'); $list = $this->renderMemberList($handles); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel(pht('Add Members')) ->setDatasource('/typeahead/common/accounts/')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue(pht('Add Members'))); $faux_form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormInsetView()) ->appendChild($list)); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Current Members (%d)', count($handles))) ->setForm($faux_form); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref('/project/view/'.$project->getID().'/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Members')) - ->setHref($this->getApplicationURI())); + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) + ->addTextCrumb( + $project->getName(), + '/project/view/'.$project->getID().'/') + ->addTextCrumb(pht('Edit Members'), $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $form_box, $box, ), array( 'title' => $title, 'device' => true, )); } private function renderMemberList(array $handles) { $request = $this->getRequest(); $user = $request->getUser(); $list = id(new PhabricatorObjectListView()) ->setHandles($handles); foreach ($handles as $handle) { $hidden_input = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'remove', 'value' => $handle->getPHID(), ), ''); $button = javelin_tag( 'button', array( 'class' => 'grey', ), pht('Remove')); $list->addButton( $handle, phabricator_form( $user, array( 'method' => 'POST', 'action' => $request->getRequestURI(), ), array($hidden_input, $button))); } return $list; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 917561419..5f7dddf37 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,296 +1,294 @@ id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needMembers(true) ->needProfiles(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $profile = $project->getProfile(); $picture = $profile->getProfileImageURI(); require_celerity_resource('phabricator-profile-css'); $tasks = $this->renderTasksPage($project, $profile); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); $query->setLimit(50); $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $feed = $this->renderStories($stories); $people = $this->renderPeoplePage($project, $profile); $content = id(new AphrontMultiColumnView()) ->addColumn($people) ->addColumn($feed) ->setFluidLayout(true); $content = phutil_tag_div( 'phabricator-project-layout', array($tasks, $content)); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setUser($user) ->setPolicyObject($project) ->setImage($picture); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { $header->setStatus('oh-ok', '', pht('Active')); } else { $header->setStatus('policy-noone', '', pht('Archived')); } $actions = $this->buildActionListView($project); $properties = $this->buildPropertyListView($project, $profile, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName())) + $crumbs->addTextCrumb($project->getName()) ->setActionList($actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => $project->getName(), 'device' => true, )); } private function renderPeoplePage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $member_phids = $project->getMemberPHIDs(); $handles = $this->loadViewerHandles($member_phids); $affiliated = array(); foreach ($handles as $phids => $handle) { $affiliated[] = phutil_tag('li', array(), $handle->renderLink()); } if ($affiliated) { $affiliated = phutil_tag('ul', array(), $affiliated); } else { $affiliated = phutil_tag('p', array(), phutil_tag('em', array(), pht('No one is affiliated with this project.'))); } return phutil_tag_div( 'phabricator-profile-info-group profile-wrap-responsive', array( phutil_tag( 'h1', array('class' => 'phabricator-profile-info-header'), pht('People')), phutil_tag_div('phabricator-profile-info-pane', $affiliated), )); } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $query->setViewer($this->getRequest()->getUser()); $query->setLimit(100); $stories = $query->execute(); if (!$stories) { return pht('There are no stories about this project.'); } return $this->renderStories($stories); } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $builder->setShowHovercards(true); $view = $builder->buildView(); return phutil_tag_div( 'profile-feed profile-wrap-responsive', $view->render()); } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $user = $this->getRequest()->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withAnyProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10); $tasks = $query->execute(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_merge( $phids, array_mergev(mpull($tasks, 'getProjectPHIDs'))); $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $task_list = new ManiphestTaskListView(); $task_list->setUser($user); $task_list->setTasks($tasks); $task_list->setHandles($handles); $list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($task_list); $content = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Open Tasks')) ->appendChild($list); return $content; } private function buildActionListView(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($project) ->setObjectURI($request->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Project')) ->setIcon('edit') ->setHref($this->getApplicationURI("edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('edit') ->setHref($this->getApplicationURI("members/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Picture')) ->setIcon('image') ->setHref($this->getApplicationURI("picture/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $action = null; if (!$project->isUserMember($viewer->getPHID())) { $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_JOIN); $action = id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm(true) ->setHref('/project/update/'.$project->getID().'/join/') ->setIcon('new') ->setDisabled(!$can_join) ->setName(pht('Join Project')); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('delete') ->setName(pht('Leave Project...')); } $view->addAction($action); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($this->getApplicationURI("history/{$id}/")) ->setIcon('transcript')); return $view; } private function buildPropertyListView( PhabricatorProject $project, PhabricatorProjectProfile $profile, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project) ->setActionList($actions); $view->addProperty( pht('Created'), phabricator_datetime($project->getDateCreated(), $viewer)); $view->addSectionHeader(pht('Description')); $view->addTextContent( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($profile->getBlurb()), 'default', $viewer)); return $view; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index 0d86c927a..db39cc159 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -1,188 +1,184 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->needProfiles(true) ->executeOne(); if (!$project) { return new Aphront404Response(); } $profile = $project->getProfile(); $options = PhabricatorProjectStatus::getStatusMap(); $e_name = true; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransaction::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransaction::TYPE_STATUS); $xaction->setNewValue($request->getStr('status')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorTransactions::TYPE_VIEW_POLICY); $xaction->setNewValue($request->getStr('can_view')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorTransactions::TYPE_EDIT_POLICY); $xaction->setNewValue($request->getStr('can_edit')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorTransactions::TYPE_JOIN_POLICY); $xaction->setNewValue($request->getStr('can_join')); $xactions[] = $xaction; $editor = new PhabricatorProjectEditor($project); $editor->setActor($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = pht('Not Unique'); $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!strlen($project->getName())) { $e_name = pht('Required'); $errors[] = pht('Project name is required.'); } else { $e_name = null; } if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } $header_name = pht('Edit Project'); $title = pht('Edit Project'); $action = '/project/edit/'.$project->getID().'/'; $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($project) ->execute(); $form = new AphrontFormView(); $form ->setID('project-edit-form') ->setUser($user) ->setAction($action) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Project Status')) ->setName('status') ->setOptions($options) ->setValue($project->getStatus())) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('blurb') ->setValue($profile->getBlurb())) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_view') ->setCaption(pht('Members can always view a project.')) ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_edit') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_join') ->setCaption( pht('Users who can edit a project can always join a project.')) ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue(pht('Save'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref('/project/view/'.$project->getID().'/')); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit Project')) - ->setHref($this->getApplicationURI())); + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) + ->addTextCrumb( + $project->getName(), + '/project/view/'.$project->getID().'/') + ->addTextCrumb(pht('Edit Project'), $this->getApplicationURI()); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php index 6188f5801..41f02eebb 100644 --- a/src/applications/project/controller/PhabricatorProjectProfilePictureController.php +++ b/src/applications/project/controller/PhabricatorProjectProfilePictureController.php @@ -1,274 +1,269 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needProfiles(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $project_uri = $this->getApplicationURI('view/'.$project->getID().'/'); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new project picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { $profile = $project->getProfile(); if ($is_default) { $profile->setProfileImagePHID(null); } else { $profile->setProfileImagePHID($xformed->getPHID()); $xformed->attachToObject($viewer, $project->getPHID()); } $profile->save(); return id(new AphrontRedirectResponse())->setURI($project_uri); } } $title = pht('Edit Project Picture'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($project->getName()) - ->setHref($project_uri)); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($project->getName(), $project_uri); + $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png'); $images = array(); $current = $project->getProfile()->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), $button); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $launch_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'launch-icon-composer', array( 'launchID' => $launch_id, 'inputID' => $input_id, )); $compose_button = javelin_tag( 'button', array( 'class' => 'grey', 'id' => $launch_id, 'sigil' => 'icon-composer', ), pht('Choose Icon and Color...')); $compose_input = javelin_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => 'phid', )); $compose_form = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), array( $compose_input, $compose_button, )); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Quick Create')) ->setValue($compose_form)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($project_uri) ->setValue(pht('Upload Picture'))); if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/releeph/controller/ReleephProjectController.php b/src/applications/releeph/controller/ReleephProjectController.php index 2a0d77497..e8516e93f 100644 --- a/src/applications/releeph/controller/ReleephProjectController.php +++ b/src/applications/releeph/controller/ReleephProjectController.php @@ -1,161 +1,155 @@ getRequest()->getUser(); // Project $project = null; $project_id = idx($data, 'projectID'); $project_name = idx($data, 'projectName'); if ($project_id) { $project = id(new ReleephProjectQuery()) ->setViewer($viewer) ->withIDs(array($project_id)) ->executeOne(); if (!$project) { throw new Exception( "ReleephProject with id '{$project_id}' not found!"); } } elseif ($project_name) { $project = id(new ReleephProject()) ->loadOneWhere('name = %s', $project_name); if (!$project) { throw new Exception( "ReleephProject with name '{$project_name}' not found!"); } } // Branch $branch = null; $branch_id = idx($data, 'branchID'); $branch_name = idx($data, 'branchName'); if ($branch_id) { $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->withIDs(array($branch_id)) ->executeOne(); if (!$branch) { throw new Exception("Branch with id '{$branch_id}' not found!"); } } elseif ($branch_name) { if (!$project) { throw new Exception( "You cannot refer to a branch by name without also referring ". "to a ReleephProject (branch names are only unique in projects)."); } $branch = id(new ReleephBranch())->loadOneWhere( 'basename = %s AND releephProjectID = %d', $branch_name, $project->getID()); if (!$branch) { throw new Exception( "ReleephBranch with basename '{$branch_name}' not found ". "in project '{$project->getName()}'!"); } // Do the branch query again, properly, to hit policies and load attached // data. // TODO: Clean this up with T3657. $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->withIDs(array($branch->getID())) ->executeOne(); if (!$branch) { throw new Exception('404!'); } } // Request $request = null; $request_id = idx($data, 'requestID'); if ($request_id) { $request = id(new ReleephRequest())->load($request_id); if (!$request) { throw new Exception( "ReleephRequest with id '{$request_id}' not found!"); } } // Fill in the gaps if ($request && !$branch) { $branch = $request->loadReleephBranch(); } if ($branch && !$project) { $project = $branch->loadReleephProject(); } // Set! $this->releephProject = $project; $this->releephBranch = $branch; $this->releephRequest = $request; } protected function getReleephProject() { if (!$this->releephProject) { throw new Exception( 'This controller did not load a ReleephProject from the URL $data.'); } return $this->releephProject; } protected function getReleephBranch() { if (!$this->releephBranch) { throw new Exception( 'This controller did not load a ReleephBranch from the URL $data.'); } return $this->releephBranch; } protected function getReleephRequest() { if (!$this->releephRequest) { throw new Exception( 'This controller did not load a ReleephRequest from the URL $data.'); } return $this->releephRequest; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); // TODO: The massive amount of derps here should be fixed once URIs get // sorted out; see T3657. try { $project = $this->getReleephProject(); $project_id = $project->getID(); $project_uri = $this->getApplicationURI("project/{$project_id}/"); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($project_uri) - ->setName($project->getName())); + $crumbs->addCrumb($project->getName(), $project_uri); } catch (Exception $ex) { // TODO: This is derps. } try { $branch = $this->getReleephBranch(); $branch_uri = $branch->getURI(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setHref($branch_uri) - ->setName($branch->getDisplayNameWithDetail())); + $crumbs->addCrumb($branch->getDisplayNameWithDetail(), $branch_uri); } catch (Exception $ex) { // TODO: This is also derps. } return $crumbs; } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php index 1309a3d55..3978571c4 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchCreateController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchCreateController.php @@ -1,117 +1,115 @@ getReleephProject(); $request = $this->getRequest(); $cut_point = $request->getStr('cutPoint'); $symbolic_name = $request->getStr('symbolicName'); if (!$cut_point) { $repository = $releeph_project->loadPhabricatorRepository(); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $cut_point = $releeph_project->getTrunkBranch(); break; } } $e_cut = true; $errors = array(); $branch_date_control = id(new AphrontFormDateControl()) ->setUser($request->getUser()) ->setName('templateDate') ->setLabel(pht('Date')) ->setCaption(pht('The date used for filling out the branch template.')) ->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY); $branch_date = $branch_date_control->readValueFromRequest($request); if ($request->isFormPost()) { $cut_commit = null; if (!$cut_point) { $e_cut = pht('Required'); $errors[] = pht('You must give a branch cut point'); } else { try { $finder = id(new ReleephCommitFinder()) ->setUser($request->getUser()) ->setReleephProject($releeph_project); $cut_commit = $finder->fromPartial($cut_point); } catch (Exception $e) { $e_cut = pht('Invalid'); $errors[] = $e->getMessage(); } } if (!$errors) { $branch = id(new ReleephBranchEditor()) ->setReleephProject($releeph_project) ->setActor($request->getUser()) ->newBranchFromCommit( $cut_commit, $branch_date, $symbolic_name); return id(new AphrontRedirectResponse()) ->setURI($branch->getURI()); } } $error_view = array(); if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle(pht('Form Errors')); } $project_id = $releeph_project->getID(); $project_uri = $this->getApplicationURI("project/{$project_id}/"); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Symbolic Name')) ->setName('symbolicName') ->setValue($symbolic_name) ->setCaption(pht('Mutable alternate name, for easy reference, '. '(e.g. "LATEST")'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Cut point')) ->setName('cutPoint') ->setValue($cut_point) ->setError($e_cut) ->setCaption( pht('A commit ID for your repo type, or a '. 'Diffusion ID like "rE123"'))) ->appendChild($branch_date_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Cut Branch')) ->addCancelButton($project_uri)); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Branch'))); + $crumbs->addTextCrumb(pht('New Branch')); return $this->buildApplicationPage( array( $crumbs, $error_view, $form, ), array( 'title' => pht('New Branch'), 'device' => true, )); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchEditController.php b/src/applications/releeph/controller/branch/ReleephBranchEditController.php index 1f1c0c6c4..3fcbf0b85 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchEditController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchEditController.php @@ -1,104 +1,102 @@ getRequest(); $releeph_branch = $this->getReleephBranch(); $symbolic_name = $request->getStr( 'symbolicName', $releeph_branch->getSymbolicName()); $errors = array(); if ($request->isFormPost()) { $existing_with_same_symbolic_name = id(new ReleephBranch()) ->loadOneWhere( 'id != %d AND releephProjectID = %d AND symbolicName = %s', $releeph_branch->getID(), $releeph_branch->getReleephProjectID(), $symbolic_name); $releeph_branch->openTransaction(); $releeph_branch ->setSymbolicName($symbolic_name); if ($existing_with_same_symbolic_name) { $existing_with_same_symbolic_name ->setSymbolicName(null) ->save(); } $releeph_branch->save(); $releeph_branch->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI($releeph_branch->getURI()); } $phids = array(); $phids[] = $creator_phid = $releeph_branch->getCreatedByUserPHID(); $phids[] = $cut_commit_phid = $releeph_branch->getCutPointCommitPHID(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($request->getUser()) ->withPHIDs($phids) ->execute(); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Branch Name')) ->setValue($releeph_branch->getName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Cut Point')) ->setValue($handles[$cut_commit_phid]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Created By')) ->setValue($handles[$creator_phid]->renderLink())) ->appendChild( id(new AphrontFormTextControl) ->setLabel(pht('Symbolic Name')) ->setName('symbolicName') ->setValue($symbolic_name) ->setCaption(pht('Mutable alternate name, for easy reference, '. '(e.g. "LATEST")'))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($releeph_branch->getURI()) ->setValue(pht('Save'))); $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_ERROR) ->setErrors($errors) ->setTitle(pht('Errors')); } $title = pht( 'Edit Branch %s', $releeph_branch->getDisplayNameWithDetail()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb(pht('Edit')); return $this->buildApplicationPage( array( $crumbs, $error_view, $form, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php index 7e1314a03..e0475d1f3 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchHistoryController.php @@ -1,50 +1,48 @@ id = $data['branchID']; parent::willProcessRequest($data); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $xactions = id(new ReleephBranchTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($branch->getPHID())) ->execute(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($branch->getPHID()) ->setTransactions($xactions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Branch History'), 'device' => true, )); } } diff --git a/src/applications/releeph/controller/project/ReleephProjectCreateController.php b/src/applications/releeph/controller/project/ReleephProjectCreateController.php index f29f52f2c..2195a4d51 100644 --- a/src/applications/releeph/controller/project/ReleephProjectCreateController.php +++ b/src/applications/releeph/controller/project/ReleephProjectCreateController.php @@ -1,172 +1,170 @@ getRequest(); $name = trim($request->getStr('name')); $trunk_branch = trim($request->getStr('trunkBranch')); $arc_pr_id = $request->getInt('arcPrID'); $arc_projects = $this->loadArcProjects(); $e_name = true; $e_trunk_branch = true; $errors = array(); if ($request->isFormPost()) { if (!$name) { $e_name = pht('Required'); $errors[] = pht( 'Your Releeph project should have a simple descriptive name.'); } if (!$trunk_branch) { $e_trunk_branch = pht('Required'); $errors[] = pht( 'You must specify which branch you will be picking from.'); } $arc_project = $arc_projects[$arc_pr_id]; $pr_repository = $arc_project->loadRepository(); if (!$errors) { $releeph_project = id(new ReleephProject()) ->setName($name) ->setTrunkBranch($trunk_branch) ->setRepositoryPHID($pr_repository->getPHID()) ->setArcanistProjectID($arc_project->getID()) ->setCreatedByUserPHID($request->getUser()->getPHID()) ->setIsActive(1); try { $releeph_project->save(); return id(new AphrontRedirectResponse()) ->setURI($releeph_project->getURI()); } catch (AphrontQueryDuplicateKeyException $ex) { $e_name = pht('Not Unique'); $errors[] = pht( 'Another project already uses this name.'); } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); } $arc_project_options = $this->getArcProjectSelectOptions($arc_projects); $project_name_input = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setDisableAutocomplete(true) ->setName('name') ->setValue($name) ->setError($e_name) ->setCaption(pht('A name like "Thrift" but not "Thrift releases".')); $arc_project_input = id(new AphrontFormSelectControl()) ->setLabel(pht('Arc Project')) ->setName('arcPrID') ->setValue($arc_pr_id) ->setCaption(pht( 'If your Arc project isn\'t listed, associate it with a repository %s', phutil_tag( 'a', array( 'href' => '/repository/', 'target' => '_blank', ), 'here'))) ->setOptions($arc_project_options); $branch_name_preview = id(new ReleephBranchPreviewView()) ->setLabel(pht('Example Branch')) ->addControl('projectName', $project_name_input) ->addControl('arcProjectID', $arc_project_input) ->addStatic('template', '') ->addStatic('isSymbolic', false); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild($project_name_input) ->appendChild($arc_project_input) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Trunk')) ->setName('trunkBranch') ->setValue($trunk_branch) ->setError($e_trunk_branch) ->setCaption(pht('The development branch, '. 'from which requests will be picked.'))) ->appendChild($branch_name_preview) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/releeph/project/') ->setValue(pht('Create'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Project')) ->setFormError($error_view) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Project'))); + $crumbs->addTextCrumb(pht('New Project')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create New Project'), 'device' => true, )); } private function loadArcProjects() { $viewer = $this->getRequest()->getUser(); $projects = id(new PhabricatorRepositoryArcanistProjectQuery()) ->setViewer($viewer) ->needRepositories(true) ->execute(); $projects = mfilter($projects, 'getRepository'); $projects = msort($projects, 'getName'); return $projects; } private function getArcProjectSelectOptions(array $arc_projects) { assert_instances_of($arc_projects, 'PhabricatorRepositoryArcanistProject'); $repos = mpull($arc_projects, 'getRepository'); $repos = mpull($repos, null, 'getID'); $groups = array(); foreach ($arc_projects as $arc_project) { $id = $arc_project->getID(); $repo_id = $arc_project->getRepository()->getID(); $groups[$repo_id][$id] = $arc_project->getName(); } $choices = array(); foreach ($groups as $repo_id => $group) { $repo_name = $repos[$repo_id]->getName(); $callsign = $repos[$repo_id]->getCallsign(); $name = "r{$callsign} ({$repo_name})"; $choices[$name] = $group; } ksort($choices); return $choices; } } diff --git a/src/applications/releeph/controller/project/ReleephProjectHistoryController.php b/src/applications/releeph/controller/project/ReleephProjectHistoryController.php index e9df27f01..85125d4cf 100644 --- a/src/applications/releeph/controller/project/ReleephProjectHistoryController.php +++ b/src/applications/releeph/controller/project/ReleephProjectHistoryController.php @@ -1,50 +1,48 @@ id = $data['projectID']; parent::willProcessRequest($data); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new ReleephProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $xactions = id(new ReleephProjectTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($project->getPHID())) ->execute(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($project->getPHID()) ->setTransactions($xactions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('History'))); + $crumbs->addTextCrumb(pht('History')); return $this->buildApplicationPage( array( $crumbs, $timeline, ), array( 'title' => pht('Project History'), 'device' => true, )); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestEditController.php b/src/applications/releeph/controller/request/ReleephRequestEditController.php index 65532155b..dc9d8abab 100644 --- a/src/applications/releeph/controller/request/ReleephRequestEditController.php +++ b/src/applications/releeph/controller/request/ReleephRequestEditController.php @@ -1,307 +1,300 @@ id = idx($data, 'requestID'); parent::willProcessRequest($data); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $releeph_project = $this->getReleephProject(); $releeph_branch = $this->getReleephBranch(); $request_identifier = $request->getStr('requestIdentifierRaw'); $e_request_identifier = true; // Load the RQ we're editing, or create a new one if ($this->id) { $rq = id(new ReleephRequestQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); $is_edit = true; } else { $is_edit = false; $rq = id(new ReleephRequest()) ->setRequestUserPHID($user->getPHID()) ->setBranchID($releeph_branch->getID()) ->setInBranch(0); } // Load all the ReleephFieldSpecifications $selector = $this->getReleephProject()->getReleephFieldSelector(); $fields = $selector->getFieldSpecifications(); foreach ($fields as $field) { $field ->setReleephProject($releeph_project) ->setReleephBranch($releeph_branch) ->setReleephRequest($rq); } $field_list = PhabricatorCustomField::getObjectFields( $rq, PhabricatorCustomField::ROLE_EDIT); foreach ($field_list->getFields() as $field) { $field ->setReleephProject($releeph_project) ->setReleephBranch($releeph_branch) ->setReleephRequest($rq); } $field_list->readFieldsFromStorage($rq); // epriestley: Is it common to pass around a referer URL to // return from whence one came? [...] // If you only have two places, maybe consider some parameter // rather than the full URL. switch ($request->getStr('origin')) { case 'request': $origin_uri = '/RQ'.$rq->getID(); break; case 'branch': default: $origin_uri = $releeph_branch->getURI(); break; } // Make edits $errors = array(); if ($request->isFormPost()) { $xactions = array(); // The commit-identifier being requested... if (!$is_edit) { if ($request_identifier === ReleephRequestTypeaheadControl::PLACEHOLDER) { $errors[] = "No commit ID was provided."; $e_request_identifier = 'Required'; } else { $pr_commit = null; $finder = id(new ReleephCommitFinder()) ->setUser($user) ->setReleephProject($releeph_project); try { $pr_commit = $finder->fromPartial($request_identifier); } catch (Exception $e) { $e_request_identifier = 'Invalid'; $errors[] = "Request {$request_identifier} is probably not a valid commit"; $errors[] = $e->getMessage(); } $pr_commit_data = null; if (!$errors) { $pr_commit_data = $pr_commit->loadCommitData(); if (!$pr_commit_data) { $e_request_identifier = 'Not parsed yet'; $errors[] = "The requested commit hasn't been parsed yet."; } } } if (!$errors) { $existing = id(new ReleephRequest()) ->loadOneWhere('requestCommitPHID = %s AND branchID = %d', $pr_commit->getPHID(), $releeph_branch->getID()); if ($existing) { return id(new AphrontRedirectResponse()) ->setURI('/releeph/request/edit/'.$existing->getID(). '?existing=1'); } $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_REQUEST) ->setNewValue($pr_commit->getPHID()); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT) // To help hide these implicit intents... ->setMetadataValue('isRQCreate', true) ->setMetadataValue('userPHID', $user->getPHID()) ->setMetadataValue( 'isAuthoritative', $releeph_project->isAuthoritative($user)) ->setNewValue(ReleephRequest::INTENT_WANT); } } // TODO: This should happen implicitly while building transactions // instead. foreach ($field_list->getFields() as $field) { $field->readValueFromRequest($request); } if (!$errors) { foreach ($fields as $field) { if ($field->isEditable()) { try { $data = $request->getRequestData(); $value = idx($data, $field->getRequiredStorageKey()); $field->validate($value); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_EDIT_FIELD) ->setMetadataValue('fieldClass', get_class($field)) ->setNewValue($value); } catch (ReleephFieldParseException $ex) { $errors[] = $ex->getMessage(); } } } } if (!$errors) { $editor = id(new ReleephRequestTransactionalEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($rq, $xactions); return id(new AphrontRedirectResponse())->setURI($origin_uri); } } $releeph_branch->populateReleephRequestHandles($user, array($rq)); $handles = $rq->getHandles(); $age_string = ''; if ($is_edit) { $age_string = phabricator_format_relative_time( time() - $rq->getDateCreated()) . ' ago'; } // Warn the user if we've been redirected here because we tried to // re-request something. $notice_view = null; if ($request->getInt('existing')) { $notice_messages = array( 'You are editing an existing pick request!', hsprintf( "Requested %s by %s", $age_string, $handles[$rq->getRequestUserPHID()]->renderLink()) ); $notice_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors($notice_messages); } /** * Build the rest of the page */ $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } $form = id(new AphrontFormView()) ->setUser($user); if ($is_edit) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Original Commit') ->setValue( $handles[$rq->getRequestCommitPHID()]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Requestor') ->setValue(hsprintf( '%s %s', $handles[$rq->getRequestUserPHID()]->renderLink(), $age_string))); } else { $origin = null; $diff_rev_id = $request->getStr('D'); if ($diff_rev_id) { $diff_rev = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withIDs(array($diff_rev_id)) ->executeOne(); $origin = '/D'.$diff_rev->getID(); $title = sprintf( 'D%d: %s', $diff_rev_id, $diff_rev->getTitle()); $form ->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Diff') ->setValue($title)); } else { $origin = $releeph_branch->getURI(); $repo = $releeph_project->loadPhabricatorRepository(); $branch_cut_point = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'phid = %s', $releeph_branch->getCutPointCommitPHID()); $form->appendChild( id(new ReleephRequestTypeaheadControl()) ->setName('requestIdentifierRaw') ->setLabel('Commit ID') ->setRepo($repo) ->setValue($request_identifier) ->setError($e_request_identifier) ->setStartTime($branch_cut_point->getEpoch()) ->setCaption( 'Start typing to autocomplete on commit title, '. 'or give a Phabricator commit identifier like rFOO1234')); } } $field_list->appendFieldsToForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($is_edit) { $title = pht('Edit Releeph Request'); $submit_name = pht('Save'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('RQ'.$rq->getID()) - ->setHref('/RQ'.$rq->getID())); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Edit'))); + $crumbs->addTextCrumb('RQ'.$rq->getID(), '/RQ'.$rq->getID()); + $crumbs->addTextCrumb(pht('Edit')); } else { $title = pht('Create Releeph Request'); $submit_name = pht('Create'); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('New Request'))); + $crumbs->addTextCrumb(pht('New Request')); } $form->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($origin_uri, 'Cancel') ->setValue($submit_name)); return $this->buildApplicationPage( array( $crumbs, $notice_view, $error_view, $form, ), array( 'title' => $title, )); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestViewController.php b/src/applications/releeph/controller/request/ReleephRequestViewController.php index fcbb6de7b..345e3acb2 100644 --- a/src/applications/releeph/controller/request/ReleephRequestViewController.php +++ b/src/applications/releeph/controller/request/ReleephRequestViewController.php @@ -1,103 +1,98 @@ getRequest(); $uri_path = $request->getRequestURI()->getPath(); $legacy_prefix = '/releeph/request/'; if (strncmp($uri_path, $legacy_prefix, strlen($legacy_prefix)) === 0) { return id(new AphrontRedirectResponse()) ->setURI('/RQ'.$this->getReleephRequest()->getID()); } $releeph_request = $this->getReleephRequest(); $releeph_branch = $this->getReleephBranch(); $releeph_project = $this->getReleephProject(); $releeph_branch->populateReleephRequestHandles( $request->getUser(), array($releeph_request)); $rq_view = id(new ReleephRequestHeaderListView()) ->setReleephProject($releeph_project) ->setReleephBranch($releeph_branch) ->setReleephRequests(array($releeph_request)) ->setUser($request->getUser()) ->setAphrontRequest($this->getRequest()) ->setReloadOnStateChange(true) ->setOriginType('request'); $user = $request->getUser(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $xactions = id(new ReleephRequestTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($releeph_request->getPHID())) ->execute(); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($request->getUser()) ->setObjectPHID($releeph_request->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment_header = pht('Plea or yield'); $draft = PhabricatorDraft::newFromUserAndKey( $user, $releeph_request->getPHID()); $title = hsprintf("RQ%d: %s", $releeph_request->getID(), $releeph_request->getSummaryForDisplay()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($releeph_request->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI( '/request/comment/'.$releeph_request->getID().'/')) ->setSubmitButtonName('Comment'); $crumbs = $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($releeph_project->getName()) - ->setHref($releeph_project->getURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($releeph_branch->getDisplayNameWithDetail()) - ->setHref($releeph_branch->getURI())) - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('RQ'.$releeph_request->getID()) - ->setHref('/RQ'.$releeph_request->getID())); + ->addTextCrumb($releeph_project->getName(), $releeph_project->getURI()) + ->addTextCrumb( + $releeph_branch->getDisplayNameWithDetail(), + $releeph_branch->getURI()) + ->addTextCrumb( + 'RQ'.$releeph_request->getID(), + '/RQ'.$releeph_request->getID()); return $this->buildStandardPageResponse( array( $crumbs, array( $rq_view, $timeline, $add_comment_form, ) ), array( 'title' => $title )); } } diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index c96c8c940..b667834c9 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -1,380 +1,375 @@ preface = $preface; return $this; } public function getPreface() { return $this->preface; } public function setQueryKey($query_key) { $this->queryKey = $query_key; return $this; } protected function getQueryKey() { return $this->queryKey; } public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } protected function getNavigation() { return $this->navigation; } public function setSearchEngine( PhabricatorApplicationSearchEngine $search_engine) { $this->searchEngine = $search_engine; return $this; } protected function getSearchEngine() { return $this->searchEngine; } protected function validateDelegatingController() { $parent = $this->getDelegatingController(); if (!$parent) { throw new Exception( "You must delegate to this controller, not invoke it directly."); } $engine = $this->getSearchEngine(); if (!$engine) { throw new Exception( "Call setEngine() before delegating to this controller!"); } $nav = $this->getNavigation(); if (!$nav) { throw new Exception( "Call setNavigation() before delegating to this controller!"); } $engine->setViewer($this->getRequest()->getUser()); $parent = $this->getDelegatingController(); $interface = 'PhabricatorApplicationSearchResultsControllerInterface'; if (!$parent instanceof $interface) { throw new Exception( "Delegating controller must implement '{$interface}'."); } } public function processRequest() { $this->validateDelegatingController(); $key = $this->getQueryKey(); if ($key == 'edit') { return $this->processEditRequest(); } else { return $this->processSearchRequest(); } } private function processSearchRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); $user = $request->getUser(); $engine = $this->getSearchEngine(); $nav = $this->getNavigation(); if ($request->isFormPost()) { $saved_query = $engine->buildSavedQueryFromRequest($request); $this->saveQuery($saved_query); return id(new AphrontRedirectResponse())->setURI( $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R'); } $named_query = null; $run_query = true; $query_key = $this->queryKey; if ($this->queryKey == 'advanced') { $run_query = false; $query_key = $request->getStr('query'); } else if (!strlen($this->queryKey)) { $found_query_data = false; if ($request->isHTTPGet()) { // If this is a GET request and it has some query data, don't // do anything unless it's only before= or after=. We'll build and // execute a query from it below. This allows external tools to build // URIs like "/query/?users=a,b". $pt_data = $request->getPassthroughRequestData(); foreach ($pt_data as $pt_key => $pt_value) { if ($pt_key != 'before' && $pt_key != 'after') { $found_query_data = true; break; } } } if (!$found_query_data) { // Otherwise, there's no query data so just run the user's default // query for this application. $query_key = head_key($engine->loadEnabledNamedQueries()); } } if ($engine->isBuiltinQuery($query_key)) { $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); } else if ($query_key) { $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($user) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved_query) { return new Aphront404Response(); } $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); } else { $saved_query = $engine->buildSavedQueryFromRequest($request); // Save the query to generate a query key, so "Save Custom Query..." and // other features like Maniphest's "Export..." work correctly. $this->saveQuery($saved_query); } $nav->selectFilter( 'query/'.$saved_query->getQueryKey(), 'query/advanced'); $form = id(new AphrontFormView()) ->setUser($user); $engine->buildSearchForm($form, $saved_query); $errors = $engine->getErrors(); if ($errors) { $run_query = false; $errors = id(new AphrontErrorView()) ->setTitle(pht('Query Errors')) ->setErrors($errors); } $submit = id(new AphrontFormSubmitControl()) ->setValue(pht('Execute Query')); if ($run_query && !$named_query && $user->isLoggedIn()) { $submit->addCancelButton( '/search/edit/'.$saved_query->getQueryKey().'/', pht('Save Custom Query...')); } $form->appendChild($submit); $filter_view = id(new AphrontListFilterView())->appendChild($form); if ($run_query && $named_query) { if ($named_query->getIsBuiltin()) { $description = pht( 'Showing results for query "%s".', $named_query->getQueryName()); } else { $description = pht( 'Showing results for saved query "%s".', $named_query->getQueryName()); } $filter_view->setCollapsed( pht('Edit Query...'), pht('Hide Query'), $description, $this->getApplicationURI('query/advanced/?query='.$query_key)); } if ($this->getPreface()) { $nav->appendChild($this->getPreface()); } $nav->appendChild($filter_view); if ($run_query) { $nav->appendChild( $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('R')); $query = $engine->buildQueryFromSavedQuery($saved_query); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); $page_size = $engine->getPageSize($saved_query); 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); } $objects = $query->setViewer($request->getUser()) ->executeWithCursorPager($pager); $list = $parent->renderResultsList($objects, $saved_query); $nav->appendChild($list); // TODO: This is a bit hacky. if ($list instanceof PHUIObjectItemListView) { $list->setNoDataString(pht("No results found for this query.")); $list->setPager($pager); } else { if ($pager->willShowPagingControls()) { $pager_box = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM) ->addMargin(PHUI::MARGIN_LARGE) ->setShadow(true) ->appendChild($pager); $nav->appendChild($pager_box); } } } if ($errors) { $nav->appendChild($errors); } if ($named_query) { $title = pht('Query: %s', $named_query->getQueryName()); } else { $title = pht('Advanced Search'); } $crumbs = $parent ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Search"))); + ->addTextCrumb(pht("Search")); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } private function processEditRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); $user = $request->getUser(); $engine = $this->getSearchEngine(); $nav = $this->getNavigation(); $named_queries = $engine->loadAllNamedQueries(); $list_id = celerity_generate_unique_node_id(); $list = new PHUIObjectItemListView(); $list->setUser($user); $list->setID($list_id); Javelin::initBehavior( 'search-reorder-queries', array( 'listID' => $list_id, 'orderURI' => '/search/order/'.get_class($engine).'/', )); foreach ($named_queries as $named_query) { $class = get_class($engine); $key = $named_query->getQueryKey(); $item = id(new PHUIObjectItemView()) ->setHeader($named_query->getQueryName()) ->setHref($engine->getQueryResultsPageURI($key)); if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { $icon = 'new'; } else { $icon = 'delete'; } $item->addAction( id(new PHUIListItemView()) ->setIcon($icon) ->setHref('/search/delete/'.$key.'/'.$class.'/') ->setWorkflow(true)); if ($named_query->getIsBuiltin()) { if ($named_query->getIsDisabled()) { $item->addIcon('delete-grey', pht('Disabled')); $item->setDisabled(true); } else { $item->addIcon('lock-grey', pht('Builtin')); } } else { $item->addAction( id(new PHUIListItemView()) ->setIcon('edit') ->setHref('/search/edit/'.$key.'/')); } $item->setGrippable(true); $item->addSigil('named-query'); $item->setMetadata( array( 'queryKey' => $named_query->getQueryKey(), )); $list->addItem($item); } $list->setNoDataString(pht('No saved queries.')); $crumbs = $parent ->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht("Saved Queries")) - ->setHref($engine->getQueryManagementURI())); + ->addTextCrumb(pht("Saved Queries"), $engine->getQueryManagementURI()); $nav->selectFilter('query/edit'); $nav->setCrumbs($crumbs); $nav->appendChild($list); return $parent->buildApplicationPage( $nav, array( 'title' => pht("Saved Queries"), 'device' => true, )); } private function saveQuery(PhabricatorSavedQuery $query) { $query->setEngineClassName(get_class($this->getSearchEngine())); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); } catch (AphrontQueryDuplicateKeyException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } protected function buildApplicationMenu() { return $this->getDelegatingController()->buildApplicationMenu(); } } diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index 78ef4be93..02f615c38 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -1,295 +1,293 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->key) { $query = id(new PhabricatorSearchQuery())->loadOneWhere( 'queryKey = %s', $this->key); if (!$query) { return new Aphront404Response(); } } else { $query = new PhabricatorSearchQuery(); if ($request->isFormPost()) { $query_str = $request->getStr('query'); $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; if ($request->getStr('jump') != 'no' && $user && $user->loadPreferences()->getPreference($pref_jump, 1)) { $response = PhabricatorJumpNavHandler::getJumpResponse( $user, $query_str); } else { $response = null; } if ($response) { return $response; } else { $query->setQuery($query_str); if ($request->getStr('scope')) { switch ($request->getStr('scope')) { case PhabricatorSearchScope::SCOPE_OPEN_REVISIONS: $query->setParameter('open', 1); $query->setParameter( 'type', DifferentialPHIDTypeRevision::TYPECONST); break; case PhabricatorSearchScope::SCOPE_OPEN_TASKS: $query->setParameter('open', 1); $query->setParameter( 'type', ManiphestPHIDTypeTask::TYPECONST); break; case PhabricatorSearchScope::SCOPE_WIKI: $query->setParameter( 'type', PhrictionPHIDTypeDocument::TYPECONST); break; case PhabricatorSearchScope::SCOPE_COMMITS: $query->setParameter( 'type', PhabricatorRepositoryPHIDTypeCommit::TYPECONST); break; default: break; } } else { if (strlen($request->getStr('type'))) { $query->setParameter('type', $request->getStr('type')); } if ($request->getArr('author')) { $query->setParameter('author', $request->getArr('author')); } if ($request->getArr('owner')) { $query->setParameter('owner', $request->getArr('owner')); } if ($request->getArr('subscribers')) { $query->setParameter('subscribers', $request->getArr('subscribers')); } if ($request->getInt('open')) { $query->setParameter('open', $request->getInt('open')); } if ($request->getArr('project')) { $query->setParameter('project', $request->getArr('project')); } } $query->save(); return id(new AphrontRedirectResponse()) ->setURI('/search/'.$query->getQueryKey().'/'); } } } $options = array( '' => 'All Documents', ) + PhabricatorSearchAbstractDocument::getSupportedTypes(); $status_options = array( 0 => 'Open and Closed Documents', 1 => 'Open Documents', ); $phids = array_merge( $query->getParameter('author', array()), $query->getParameter('owner', array()), $query->getParameter('subscribers', array()), $query->getParameter('project', array())); $handles = $this->loadViewerHandles($phids); $author_value = array_select_keys( $handles, $query->getParameter('author', array())); $owner_value = array_select_keys( $handles, $query->getParameter('owner', array())); $subscribers_value = array_select_keys( $handles, $query->getParameter('subscribers', array())); $project_value = array_select_keys( $handles, $query->getParameter('project', array())); $search_form = new AphrontFormView(); $search_form ->setUser($user) ->setAction('/search/') ->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'jump', 'value' => 'no', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Search') ->setName('query') ->setValue($query->getQuery())) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Document Type') ->setName('type') ->setOptions($options) ->setValue($query->getParameter('type'))) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Document Status') ->setName('open') ->setOptions($status_options) ->setValue($query->getParameter('open'))) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('author') ->setLabel('Author') ->setDatasource('/typeahead/common/users/') ->setValue($author_value)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('owner') ->setLabel('Owner') ->setDatasource('/typeahead/common/searchowner/') ->setValue($owner_value) ->setCaption( 'Tip: search for "Up For Grabs" to find unowned documents.')) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('subscribers') ->setLabel('Subscribers') ->setDatasource('/typeahead/common/users/') ->setValue($subscribers_value)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('project') ->setLabel('Project') ->setDatasource('/typeahead/common/projects/') ->setValue($project_value)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Search')); $search_panel = new AphrontListFilterView(); $search_panel->appendChild($search_form); require_celerity_resource('phabricator-search-results-css'); if ($query->getID()) { $limit = 20; $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'page'); $pager->setPageSize($limit); $pager->setOffset($request->getInt('page')); $query->setParameter('limit', $limit + 1); $query->setParameter('offset', $pager->getOffset()); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $results = $engine->executeSearch($query); $results = $pager->sliceResults($results); // If there are any objects which match the query by name, and we're // not paging through the results, prefix the results with the named // objects. if (!$request->getInt('page')) { $named = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withNames(array($query->getQuery())) ->execute(); if ($named) { $results = array_merge(array_keys($named), $results); } } if ($results) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($results) ->execute(); $objects = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withPHIDs($results) ->execute(); $results = array(); foreach ($handles as $phid => $handle) { $view = id(new PhabricatorSearchResultView()) ->setHandle($handle) ->setQuery($query) ->setObject(idx($objects, $phid)); $results[] = $view->render(); } $results = phutil_tag_div('phabricator-search-result-list', array( phutil_implode_html("\n", $results), phutil_tag_div('search-results-pager', $pager->render()), )); } else { $results = phutil_tag_div( 'phabricator-search-result-list', phutil_tag( 'p', array('class' => 'phabricator-search-no-results'), pht('No search results.'))); } $results = id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) ->addPadding(PHUI::PADDING_LARGE) ->setShadow(true) ->appendChild($results) ->addClass('phabricator-search-result-box'); } else { $results = null; } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName(pht('Search'))); + $crumbs->addTextCrumb(pht('Search')); return $this->buildApplicationPage( array( $crumbs, $search_panel, $results, ), array( 'title' => pht('Search Results'), 'device' => true, )); } } diff --git a/src/applications/search/controller/PhabricatorSearchEditController.php b/src/applications/search/controller/PhabricatorSearchEditController.php index 4d84af847..f85f94f76 100644 --- a/src/applications/search/controller/PhabricatorSearchEditController.php +++ b/src/applications/search/controller/PhabricatorSearchEditController.php @@ -1,117 +1,115 @@ queryKey = idx($data, 'queryKey'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($user) ->withQueryKeys(array($this->queryKey)) ->executeOne(); if (!$saved_query) { return new Aphront404Response(); } $engine = $saved_query->newEngine()->setViewer($user); $complete_uri = $engine->getQueryManagementURI(); $cancel_uri = $complete_uri; $named_query = id(new PhabricatorNamedQueryQuery()) ->setViewer($user) ->withQueryKeys(array($saved_query->getQueryKey())) ->withUserPHIDs(array($user->getPHID())) ->executeOne(); if (!$named_query) { $named_query = id(new PhabricatorNamedQuery()) ->setUserPHID($user->getPHID()) ->setQueryKey($saved_query->getQueryKey()) ->setEngineClassName($saved_query->getEngineClassName()); // If we haven't saved the query yet, this is a "Save..." operation, so // take the user back to the query if they cancel instead of back to the // management interface. $cancel_uri = $engine->getQueryResultsPageURI( $saved_query->getQueryKey()); } $e_name = true; $errors = array(); if ($request->isFormPost()) { $named_query->setQueryName($request->getStr('name')); if (!strlen($named_query->getQueryName())) { $e_name = pht('Required'); $errors[] = pht('You must name the query.'); } else { $e_name = null; } if (!$errors) { $named_query->save(); return id(new AphrontRedirectResponse())->setURI($complete_uri); } } if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($user); $form->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Query Name')) ->setValue($named_query->getQueryName()) ->setError($e_name)); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Query')) ->addCancelButton($cancel_uri)); if ($named_query->getID()) { $title = pht('Edit Saved Query'); } else { $title = pht('Save Query'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php index f4409e756..a94a15872 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteEditController.php @@ -1,267 +1,265 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $is_new = false; } else { $poll = PhabricatorSlowvotePoll::initializeNewPoll($user); $is_new = true; } $e_question = true; $e_response = true; $errors = array(); $v_question = $poll->getQuestion(); $v_description = $poll->getDescription(); $v_responses = $poll->getResponseVisibility(); $v_shuffle = $poll->getShuffle(); $responses = $request->getArr('response'); if ($request->isFormPost()) { $v_question = $request->getStr('question'); $v_description = $request->getStr('description'); $v_responses = (int)$request->getInt('responses'); $v_shuffle = (int)$request->getBool('shuffle'); $v_view_policy = $request->getStr('viewPolicy'); if ($is_new) { $poll->setMethod($request->getInt('method')); } if (!strlen($v_question)) { $e_question = pht('Required'); $errors[] = pht('You must ask a poll question.'); } else { $e_question = null; } if ($is_new) { $responses = array_filter($responses); if (empty($responses)) { $errors[] = pht('You must offer at least one response.'); $e_response = pht('Required'); } else { $e_response = null; } } $xactions = array(); $template = id(new PhabricatorSlowvoteTransaction()); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_QUESTION) ->setNewValue($v_question); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION) ->setNewValue($v_description); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_RESPONSES) ->setNewValue($v_responses); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorSlowvoteTransaction::TYPE_SHUFFLE) ->setNewValue($v_shuffle); $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view_policy); if (empty($errors)) { $editor = id(new PhabricatorSlowvoteEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($poll, $xactions); if ($is_new) { $poll->save(); foreach ($responses as $response) { $option = new PhabricatorSlowvoteOption(); $option->setName($response); $option->setPollID($poll->getID()); $option->save(); } } return id(new AphrontRedirectResponse()) ->setURI('/V'.$poll->getID()); } else { $poll->setViewPolicy($v_view_policy); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } $instructions = phutil_tag( 'p', array( 'class' => 'aphront-form-instructions', ), pht('Resolve issues and build consensus through '. 'protracted deliberation.')); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild($instructions) ->appendChild( id(new AphrontFormTextAreaControl()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setLabel(pht('Question')) ->setName('question') ->setValue($v_question) ->setError($e_question)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($v_description)); if ($is_new) { for ($ii = 0; $ii < 10; $ii++) { $n = ($ii + 1); $response = id(new AphrontFormTextControl()) ->setLabel(pht("Response %d", $n)) ->setName('response[]') ->setValue(idx($responses, $ii, '')); if ($ii == 0) { $response->setError($e_response); } $form->appendChild($response); } } $poll_type_options = array( PhabricatorSlowvotePoll::METHOD_PLURALITY => pht('Plurality (Single Choice)'), PhabricatorSlowvotePoll::METHOD_APPROVAL => pht('Approval (Multiple Choice)'), ); $response_type_options = array( PhabricatorSlowvotePoll::RESPONSES_VISIBLE => pht('Allow anyone to see the responses'), PhabricatorSlowvotePoll::RESPONSES_VOTERS => pht('Require a vote to see the responses'), PhabricatorSlowvotePoll::RESPONSES_OWNER => pht('Only I can see the responses'), ); if ($is_new) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Vote Type')) ->setName('method') ->setValue($poll->getMethod()) ->setOptions($poll_type_options)); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Vote Type')) ->setValue(idx($poll_type_options, $poll->getMethod()))); } if ($is_new) { $title = pht('Create Slowvote'); $button = pht('Create'); $cancel_uri = $this->getApplicationURI(); } else { $title = pht('Edit %s', 'V'.$poll->getID()); $button = pht('Save Changes'); $cancel_uri = '/V'.$poll->getID(); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($poll) ->execute(); $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Responses')) ->setName('responses') ->setValue($v_responses) ->setOptions($response_type_options)) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Shuffle')) ->addCheckbox( 'shuffle', 1, pht('Show choices in random order.'), $v_shuffle)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('viewPolicy') ->setPolicyObject($poll) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title)); + $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormError($error_view) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php index 274a85b6a..1ceeda6cb 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php @@ -1,185 +1,183 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needOptions(true) ->needChoices(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } $poll_view = id(new SlowvoteEmbedView()) ->setHeadless(true) ->setUser($user) ->setPoll($poll); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'pollID' => $poll->getID(), 'contentHTML' => $poll_view->render(), )); } $header = id(new PHUIHeaderView()) ->setHeader($poll->getQuestion()) ->setUser($user) ->setPolicyObject($poll); $actions = $this->buildActionView($poll); $properties = $this->buildPropertyView($poll, $actions); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addCrumb( - id(new PhabricatorCrumbView()) - ->setName('V'.$poll->getID())); + $crumbs->addTextCrumb('V'.$poll->getID()); $xactions = $this->buildTransactions($poll); $add_comment = $this->buildCommentForm($poll); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, phutil_tag( 'div', array( 'class' => 'mlt mml mmr', ), $poll_view), $xactions, $add_comment, ), array( 'title' => 'V'.$poll->getID().' '.$poll->getQuestion(), 'device' => true, 'pageObjects' => array($poll->getPHID()), )); } private function buildActionView(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($poll); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $poll, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Poll')) ->setIcon('edit') ->setHref($this->getApplicationURI('edit/'.$poll->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); return $view; } private function buildPropertyView( PhabricatorSlowvotePoll $poll, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($poll) ->setActionList($actions); $view->invokeWillRenderEvent(); if (strlen($poll->getDescription())) { $view->addTextContent( $output = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent( $poll->getDescription()), 'default', $viewer)); } return $view; } private function buildTransactions(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $xactions = id(new PhabricatorSlowvoteTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($poll->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($poll->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); return $timeline; } private function buildCommentForm(PhabricatorSlowvotePoll $poll) { $viewer = $this->getRequest()->getUser(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Enter Deliberations'); $submit_button_name = $is_serious ? pht('Add Comment') : pht('Perhaps'); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $poll->getPHID()); return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($poll->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$poll->getID().'/')) ->setSubmitButtonName($submit_button_name); } } diff --git a/src/applications/tokens/controller/PhabricatorTokenGivenController.php b/src/applications/tokens/controller/PhabricatorTokenGivenController.php index 471bb47fc..1928c89fe 100644 --- a/src/applications/tokens/controller/PhabricatorTokenGivenController.php +++ b/src/applications/tokens/controller/PhabricatorTokenGivenController.php @@ -1,79 +1,77 @@ getRequest(); $user = $request->getUser(); $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $tokens_given = id(new PhabricatorTokenGivenQuery()) ->setViewer($user) ->executeWithCursorPager($pager); $handles = array(); if ($tokens_given) { $object_phids = mpull($tokens_given, 'getObjectPHID'); $user_phids = mpull($tokens_given, 'getAuthorPHID'); $handle_phids = array_merge($object_phids, $user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($handle_phids) ->execute(); } $tokens = array(); if ($tokens_given) { $token_phids = mpull($tokens_given, 'getTokenPHID'); $tokens = id(new PhabricatorTokenQuery()) ->setViewer($user) ->withPHIDs($token_phids) ->execute(); $tokens = mpull($tokens, null, 'getPHID'); } $list = new PHUIObjectItemListView(); foreach ($tokens_given as $token_given) { $handle = $handles[$token_given->getObjectPHID()]; $token = idx($tokens, $token_given->getTokenPHID()); $item = id(new PHUIObjectItemView()); $item->setHeader($handle->getFullName()); $item->setHref($handle->getURI()); $item->addAttribute($token->renderIcon()); $item->addAttribute( pht( 'Given by %s on %s', $handles[$token_given->getAuthorPHID()]->renderLink(), phabricator_date($token_given->getDateCreated(), $user))); $list->addItem($item); } $list->setPager($pager); $title = pht('Tokens Given'); $nav = $this->buildSideNav(); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title))); + ->addTextCrumb($title)); $nav->selectFilter('given/'); $nav->appendChild($list); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/tokens/controller/PhabricatorTokenLeaderController.php b/src/applications/tokens/controller/PhabricatorTokenLeaderController.php index 271a5f1ab..50527488b 100644 --- a/src/applications/tokens/controller/PhabricatorTokenLeaderController.php +++ b/src/applications/tokens/controller/PhabricatorTokenLeaderController.php @@ -1,61 +1,59 @@ getRequest(); $user = $request->getUser(); $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'page'); $pager->setOffset($request->getInt('page')); $query = id(new PhabricatorTokenReceiverQuery()); $objects = $query->setViewer($user)->executeWithOffsetPager($pager); $counts = $query->getTokenCounts(); $handles = array(); $phids = array(); if ($counts) { $phids = mpull($objects, 'getPHID'); $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($phids) ->execute(); } $list = new PHUIObjectItemListView(); foreach ($phids as $object) { $count = idx($counts, $object, 0); $item = id(new PHUIObjectItemView()); $handle = $handles[$object]; $item->setHeader($handle->getFullName()); $item->setHref($handle->getURI()); $item->addAttribute(pht('Tokens: %s', $count)); $list->addItem($item); } $title = pht('Token Leader Board'); $nav = $this->buildSideNav(); $nav->setCrumbs( $this->buildApplicationCrumbs() - ->addCrumb( - id(new PhabricatorCrumbView()) - ->setName($title))); + ->addTextCrumb($title)); $nav->selectFilter('leaders/'); $nav->appendChild($list); $nav->appendChild($pager); return $this->buildApplicationPage( $nav, array( 'title' => $title, 'device' => true, )); } } diff --git a/src/view/layout/PhabricatorCrumbsView.php b/src/view/layout/PhabricatorCrumbsView.php index f9c378655..ec411ca6e 100644 --- a/src/view/layout/PhabricatorCrumbsView.php +++ b/src/view/layout/PhabricatorCrumbsView.php @@ -1,142 +1,158 @@ addCrumb( + id(new PhabricatorCrumbView()) + ->setName($text) + ->setHref($href)); + } + public function addCrumb(PhabricatorCrumbView $crumb) { $this->crumbs[] = $crumb; return $this; } public function addAction(PHUIListItemView $action) { $this->actions[] = $action; return $this; } public function setActionList(PhabricatorActionListView $list) { $this->actionListID = celerity_generate_unique_node_id(); $list->setId($this->actionListID); return $this; } public function render() { require_celerity_resource('phabricator-crumbs-view-css'); $action_view = null; if (($this->actions) || ($this->actionListID)) { $actions = array(); foreach ($this->actions as $action) { $icon = null; if ($action->getIcon()) { $icon_name = $action->getIcon(); if ($action->getDisabled()) { $icon_name .= '-grey'; } $icon = phutil_tag( 'span', array( 'class' => 'sprite-icons icons-'.$icon_name, ), ''); } $name = phutil_tag( 'span', array( 'class' => 'phabricator-crumbs-action-name' ), $action->getName()); $action_sigils = $action->getSigils(); if ($action->getWorkflow()) { $action_sigils[] = 'workflow'; } $action_classes = $action->getClasses(); $action_classes[] = 'phabricator-crumbs-action'; if ($action->getDisabled()) { $action_classes[] = 'phabricator-crumbs-action-disabled'; } $actions[] = javelin_tag( 'a', array( 'href' => $action->getHref(), 'class' => implode(' ', $action_classes), 'sigil' => implode(' ', $action_sigils), 'style' => $action->getStyle() ), array( $icon, $name, )); } if ($this->actionListID) { $icon_id = celerity_generate_unique_node_id(); $icon = phutil_tag( 'span', array( 'class' => 'sprite-icons action-action-menu' ), ''); $name = phutil_tag( 'span', array( 'class' => 'phabricator-crumbs-action-name' ), pht('Actions')); $actions[] = javelin_tag( 'a', array( 'href' => '#', 'class' => 'phabricator-crumbs-action phabricator-crumbs-action-menu', 'sigil' => 'jx-toggle-class', 'id' => $icon_id, 'meta' => array( 'map' => array( $this->actionListID => 'phabricator-action-list-toggle', $icon_id => 'phabricator-crumbs-action-menu-open' ), ), ), array( $icon, $name, )); } $action_view = phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-actions', ), $actions); } if ($this->crumbs) { last($this->crumbs)->setIsLastCrumb(true); } return phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-view '. 'sprite-gradient gradient-breadcrumbs', ), array( $action_view, $this->crumbs, )); } }