diff --git a/src/applications/auth/controller/PhabricatorEmailTokenController.php b/src/applications/auth/controller/PhabricatorEmailTokenController.php index e907a7dcb..0964a7a96 100644 --- a/src/applications/auth/controller/PhabricatorEmailTokenController.php +++ b/src/applications/auth/controller/PhabricatorEmailTokenController.php @@ -1,92 +1,93 @@ token = $data['token']; } public function processRequest() { $request = $this->getRequest(); $token = $this->token; $email = $request->getStr('email'); // NOTE: We need to bind verification to **addresses**, not **users**, // because we verify addresses when they're used to login this way, and if // we have a user-based verification you can: // // - Add some address you do not own; // - request a password reset; // - change the URI in the email to the address you don't own; // - login via the email link; and // - get a "verified" address you don't control. $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_email || !$target_user || !$target_user->validateEmailToken($target_email, $token)) { $view = new AphrontRequestFailureView(); $view->setHeader(pht('Unable to Login')); $view->appendChild(phutil_tag('p', array(), pht( 'The authentication information in the link you clicked is '. 'invalid or out of date. Make sure you are copy-and-pasting the '. 'entire link into your browser. You can try again, or request '. 'a new email.'))); - $view->appendChild(hsprintf( - '
'. - '%s'. - '
', - pht('Send Another Email'))); + $view->appendChild(phutil_tag_div( + 'aphront-failure-continue', + phutil_tag( + 'a', + array('class' => 'button', 'href' => '/login/email/'), + pht('Send Another Email')))); return $this->buildStandardPageResponse( $view, array( 'title' => pht('Login Failure'), )); } // Verify email so that clicking the link in the "Welcome" email is good // enough, without requiring users to go through a second round of email // verification. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $target_email->setIsVerified(1); $target_email->save(); unset($unguarded); $next = '/'; if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { $next = '/settings/panel/external/'; } else if (PhabricatorEnv::getEnvConfig('account.editable')) { $next = (string)id(new PhutilURI('/settings/panel/password/')) ->setQueryParams( array( 'token' => $token, 'email' => $email, )); } $request->setCookie('next_uri', $next); return $this->loginUser($target_user); } } diff --git a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php index 67a253b40..7b580a1ef 100644 --- a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php +++ b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php @@ -1,77 +1,77 @@ getRequest(); $user = $request->getUser(); $email = $user->loadPrimaryEmail(); if ($email->getIsVerified()) { return id(new AphrontRedirectResponse())->setURI('/'); } $email_address = $email->getAddress(); $sent = null; if ($request->isFormPost()) { $email->sendVerificationEmail($user); $sent = new AphrontErrorView(); $sent->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $sent->setTitle(pht('Email Sent')); $sent->appendChild(phutil_tag( 'p', array(), pht( 'Another verification email was sent to %s.', phutil_tag('strong', array(), $email_address)))); } $error_view = new AphrontRequestFailureView(); $error_view->setHeader(pht('Check Your Email')); $error_view->appendChild(phutil_tag('p', array(), pht( 'You must verify your email address to login. You should have a new '. 'email message from Phabricator with verification instructions in your '. 'inbox (%s).', phutil_tag('strong', array(), $email_address)))); $error_view->appendChild(phutil_tag('p', array(), pht( 'If you did not receive an email, you can click the button below '. 'to try sending another one.'))); - $error_view->appendChild(hsprintf( - '
%s
', + $error_view->appendChild(phutil_tag_div( + 'aphront-failure-continue', phabricator_form( $user, array( 'action' => '/login/mustverify/', 'method' => 'POST', ), phutil_tag( 'button', array( ), pht('Send Another Email'))))); return $this->buildApplicationPage( array( $sent, $error_view, ), array( 'title' => pht('Must Verify Email'), 'device' => true )); } } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php index 6b68b4249..9c4a365fc 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderLDAP.php @@ -1,393 +1,393 @@ setProperty(self::KEY_PORT, 389) ->setProperty(self::KEY_VERSION, 3); } public function getAdapter() { if (!$this->adapter) { $conf = $this->getProviderConfig(); $realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES); if (!is_array($realname_attributes)) { $realname_attributes = array(); } $adapter = id(new PhutilAuthAdapterLDAP()) ->setHostname( $conf->getProperty(self::KEY_HOSTNAME)) ->setPort( $conf->getProperty(self::KEY_PORT)) ->setBaseDistinguishedName( $conf->getProperty(self::KEY_DISTINGUISHED_NAME)) ->setSearchAttribute( $conf->getProperty(self::KEY_SEARCH_ATTRIBUTE)) ->setUsernameAttribute( $conf->getProperty(self::KEY_USERNAME_ATTRIBUTE)) ->setRealNameAttributes($realname_attributes) ->setLDAPVersion( $conf->getProperty(self::KEY_VERSION)) ->setLDAPReferrals( $conf->getProperty(self::KEY_REFERRALS)) ->setLDAPStartTLS( $conf->getProperty(self::KEY_START_TLS)) ->setAnonymousUsername( $conf->getProperty(self::KEY_ANONYMOUS_USERNAME)) ->setAnonymousPassword( new PhutilOpaqueEnvelope( $conf->getProperty(self::KEY_ANONYMOUS_PASSWORD))) ->setSearchFirst( $conf->getProperty(self::KEY_SEARCH_FIRST)) ->setActiveDirectoryDomain( $conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN)); $this->adapter = $adapter; } return $this->adapter; } protected function renderLoginForm(AphrontRequest $request, $mode) { $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setSubmitURI($this->getLoginURI()) ->setUser($viewer); if ($mode == 'link') { $dialog->setTitle(pht('Link LDAP Account')); $dialog->addSubmitButton(pht('Link Accounts')); $dialog->addCancelButton($this->getSettingsURI()); } else if ($mode == 'refresh') { $dialog->setTitle(pht('Refresh LDAP Account')); $dialog->addSubmitButton(pht('Refresh Account')); $dialog->addCancelButton($this->getSettingsURI()); } else { if ($this->shouldAllowRegistration()) { $dialog->setTitle(pht('Login or Register with LDAP')); $dialog->addSubmitButton(pht('Login or Register')); } else { $dialog->setTitle(pht('Login with LDAP')); $dialog->addSubmitButton(pht('Login')); } if ($mode == 'login') { $dialog->addCancelButton($this->getStartURI()); } } $v_user = $request->getStr('ldap_username'); $e_user = null; $e_pass = null; $errors = array(); if ($request->isHTTPPost()) { // NOTE: This is intentionally vague so as not to disclose whether a // given username exists. $e_user = pht('Invalid'); $e_pass = pht('Invalid'); $errors[] = pht('Username or password are incorrect.'); } $form = id(new PHUIFormLayoutView()) ->setUser($viewer) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('LDAP Username') ->setName('ldap_username') ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel('LDAP Password') ->setName('ldap_password') ->setError($e_pass)); if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $dialog->appendChild($errors); $dialog->appendChild($form); return $dialog; } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $viewer = $request->getUser(); $response = null; $account = null; $username = $request->getStr('ldap_username'); $password = $request->getStr('ldap_password'); $has_password = strlen($password); $password = new PhutilOpaqueEnvelope($password); if (!strlen($username) || !$has_password) { $response = $controller->buildProviderPageResponse( $this, $this->renderLoginForm($request, 'login')); return array($account, $response); } try { if (strlen($username) && $has_password) { $adapter = $this->getAdapter(); $adapter->setLoginUsername($username); $adapter->setLoginPassword($password); // TODO: This calls ldap_bind() eventually, which dumps cleartext // passwords to the error log. See note in PhutilAuthAdapterLDAP. // See T3351. DarkConsoleErrorLogPluginAPI::enableDiscardMode(); $account_id = $adapter->getAccountID(); DarkConsoleErrorLogPluginAPI::disableDiscardMode(); } else { throw new Exception("Username and password are required!"); } } catch (Exception $ex) { // TODO: Make this cleaner. throw $ex; } return array($this->loadOrCreateAccount($account_id), $response); } const KEY_HOSTNAME = 'ldap:host'; const KEY_PORT = 'ldap:port'; const KEY_DISTINGUISHED_NAME = 'ldap:dn'; const KEY_SEARCH_ATTRIBUTE = 'ldap:search-attribute'; const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute'; const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes'; const KEY_VERSION = 'ldap:version'; const KEY_REFERRALS = 'ldap:referrals'; const KEY_START_TLS = 'ldap:start-tls'; const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username'; const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password'; const KEY_SEARCH_FIRST = 'ldap:search-first'; const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain'; private function getPropertyKeys() { return array_keys($this->getPropertyLabels()); } private function getPropertyLabels() { return array( self::KEY_HOSTNAME => pht('LDAP Hostname'), self::KEY_PORT => pht('LDAP Port'), self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'), self::KEY_SEARCH_ATTRIBUTE => pht('Search Attribute'), self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'), self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'), self::KEY_VERSION => pht('LDAP Version'), self::KEY_REFERRALS => pht('Enable Referrals'), self::KEY_START_TLS => pht('Use TLS'), self::KEY_SEARCH_FIRST => pht('Search First'), self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'), self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'), self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'), ); } public function readFormValuesFromProvider() { $properties = array(); foreach ($this->getPropertyLabels() as $key => $ignored) { $properties[$key] = $this->getProviderConfig()->getProperty($key); } return $properties; } public function readFormValuesFromRequest(AphrontRequest $request) { $values = array(); foreach ($this->getPropertyKeys() as $key) { switch ($key) { case self::KEY_REALNAME_ATTRIBUTES: $values[$key] = $request->getStrList($key, array()); break; default: $values[$key] = $request->getStr($key); break; } } return $values; } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { $labels = $this->getPropertyLabels(); $captions = array( self::KEY_HOSTNAME => pht('Example: %s', - hsprintf('%s', pht('ldap.example.com'))), + phutil_tag('tt', array(), pht('ldap.example.com'))), self::KEY_DISTINGUISHED_NAME => pht('Example: %s', - hsprintf('%s', pht('ou=People, dc=example, dc=com'))), + phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))), self::KEY_SEARCH_ATTRIBUTE => pht('Example: %s', - hsprintf('%s', pht('sn'))), + phutil_tag('tt', array(), pht('sn'))), self::KEY_USERNAME_ATTRIBUTE => pht('Optional, if different from search attribute.'), self::KEY_REALNAME_ATTRIBUTES => pht('Optional. Example: %s', - hsprintf('%s', pht('firstname, lastname'))), + phutil_tag('tt', array(), pht('firstname, lastname'))), self::KEY_REFERRALS => pht('Follow referrals. Disable this for Windows AD 2003.'), self::KEY_START_TLS => pht('Start TLS after binding to the LDAP server.'), self::KEY_SEARCH_FIRST => pht( 'When the user enters their username, search for a matching '. 'record using the "Search Attribute", then try to bind using '. 'the DN for the record. This is useful if usernames are not '. 'part of the record DN.'), self::KEY_ANONYMOUS_USERNAME => pht('Username to bind with before searching.'), self::KEY_ANONYMOUS_PASSWORD => pht('Password to bind with before searching.'), ); $types = array( self::KEY_REFERRALS => 'checkbox', self::KEY_START_TLS => 'checkbox', self::KEY_SEARCH_FIRST => 'checkbox', self::KEY_REALNAME_ATTRIBUTES => 'list', self::KEY_ANONYMOUS_PASSWORD => 'password', ); foreach ($labels as $key => $label) { $caption = idx($captions, $key); $type = idx($types, $key); $value = idx($values, $key); $control = null; switch ($type) { case 'checkbox': $control = id(new AphrontFormCheckboxControl()) ->addCheckbox( $key, 1, hsprintf('%s: %s', $label, $caption), $value); break; case 'list': $control = id(new AphrontFormTextControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value ? implode(', ', $value) : null); break; case 'password': $control = id(new AphrontFormPasswordControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value); break; default: $control = id(new AphrontFormTextControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value); break; } $form->appendChild($control); } } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); $labels = $this->getPropertyLabels(); if (isset($labels[$key])) { $label = $labels[$key]; $mask = false; switch ($key) { case self::KEY_ANONYMOUS_PASSWORD: $mask = true; break; } if ($mask) { return pht( '%s updated the "%s" value.', $xaction->renderHandleLink($author_phid), $label); } if (!strlen($old)) { return pht( '%s set the "%s" value to "%s".', $xaction->renderHandleLink($author_phid), $label, $new); } else { return pht( '%s changed the "%s" value from "%s" to "%s".', $xaction->renderHandleLink($author_phid), $label, $old, $new); } } return parent::renderConfigPropertyTransactionTitle($xaction); } public static function getLDAPProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { if ($provider instanceof PhabricatorAuthProviderLDAP) { return $provider; } } return null; } } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php index 0fe516968..10f7a5921 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php @@ -1,128 +1,126 @@ getDomain()); } public function getDefaultProviderConfig() { return parent::getDefaultProviderConfig() ->setProperty(self::KEY_REQUIRE_SECURE, 1); } protected function newOAuthAdapter() { $require_secure = $this->getProviderConfig()->getProperty( self::KEY_REQUIRE_SECURE); return id(new PhutilAuthAdapterOAuthFacebook()) ->setRequireSecureBrowsing($require_secure); } protected function getLoginIcon() { return 'Facebook'; } public function readFormValuesFromProvider() { $require_secure = $this->getProviderConfig()->getProperty( self::KEY_REQUIRE_SECURE); return parent::readFormValuesFromProvider() + array( self::KEY_REQUIRE_SECURE => $require_secure, ); } public function readFormValuesFromRequest(AphrontRequest $request) { return parent::readFormValuesFromRequest($request) + array( self::KEY_REQUIRE_SECURE => $request->getBool(self::KEY_REQUIRE_SECURE), ); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { parent::extendEditForm($request, $form, $values, $issues); $key_require = self::KEY_REQUIRE_SECURE; $v_require = idx($values, $key_require); $form ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( $key_require, $v_require, pht( "%s ". "Require users to enable 'secure browsing' on Facebook in order ". "to use Facebook to authenticate with Phabricator. This ". "improves security by preventing an attacker from capturing ". "an insecure Facebook session and escalating it into a ". "Phabricator session. Enabling it is recommended.", - hsprintf( - '%s', - pht('Require Secure Browsing:'))))); + phutil_tag('strong', array(), pht('Require Secure Browsing:'))))); } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); switch ($key) { case self::KEY_REQUIRE_SECURE: if ($new) { return pht( '%s turned "Require Secure Browsing" on.', $xaction->renderHandleLink($author_phid)); } else { return pht( '%s turned "Require Secure Browsing" off.', $xaction->renderHandleLink($author_phid)); } } return parent::renderConfigPropertyTransactionTitle($xaction); } public static function getFacebookApplicationID() { $providers = PhabricatorAuthProvider::getAllProviders(); $fb_provider = idx($providers, 'facebook:facebook.com'); if (!$fb_provider) { return null; } return $fb_provider->getProviderConfig()->getProperty( PhabricatorAuthProviderOAuth::PROPERTY_APP_ID); } } diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 5449546b0..d4cbd60de 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -1,410 +1,411 @@ getRequest(); if ($request->getUser()) { // NOTE: Unit tests can set a user explicitly. Normal requests are not // permitted to do this. PhabricatorTestCase::assertExecutingUnitTests(); $user = $request->getUser(); } else { $user = new PhabricatorUser(); $phusr = $request->getCookie('phusr'); $phsid = $request->getCookie('phsid'); if (strlen($phusr) && $phsid) { $info = queryfx_one( $user->establishConnection('r'), 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID AND s.type LIKE %> AND s.sessionKey = %s', $user->getTableName(), 'phabricator_session', 'web-', PhabricatorHash::digest($phsid)); if ($info) { $user->loadFromArray($info); } } $request->setUser($user); } $translation = $user->getTranslation(); if ($translation && $translation != PhabricatorEnv::getEnvConfig('translation.provider')) { $translation = newv($translation, array()); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); } $preferences = $user->loadPreferences(); if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) { $dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE; if ($preferences->getPreference($dark_console) || PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { $console = new DarkConsoleCore(); $request->getApplicationConfiguration()->setConsole($console); } } if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) { $disabled_user_controller = new PhabricatorDisabledUserController( $request); return $this->delegateToController($disabled_user_controller); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST, array( 'request' => $request, 'controller' => $this, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $checker_controller = $event->getValue('controller'); if ($checker_controller != $this) { return $this->delegateToController($checker_controller); } if ($this->shouldRequireLogin()) { // This actually means we need either: // - a valid user, or a public controller; and // - permission to see the application. $auth_class = 'PhabricatorApplicationAuth'; $auth_application = PhabricatorApplication::getByClass($auth_class); $allow_public = $this->shouldAllowPublic() && PhabricatorEnv::getEnvConfig('policy.allow-public'); // If this controller isn't public, and the user isn't logged in, require // login. if (!$allow_public && !$user->isLoggedIn()) { $login_controller = new PhabricatorAuthStartController($request); $this->setCurrentApplication($auth_application); return $this->delegateToController($login_controller); } if ($user->isLoggedIn()) { if ($this->shouldRequireEmailVerification()) { $email = $user->loadPrimaryEmail(); if (!$email) { throw new Exception( "No primary email address associated with this account!"); } if (!$email->getIsVerified()) { $controller = new PhabricatorMustVerifyEmailController($request); $this->setCurrentApplication($auth_application); return $this->delegateToController($controller); } } } // If the user doesn't have access to the application, don't let them use // any of its controllers. We query the application in order to generate // a policy exception if the viewer doesn't have permission. $application = $this->getCurrentApplication(); if ($application) { id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withPHIDs(array($application->getPHID())) ->executeOne(); } } // NOTE: We do this last so that users get a login page instead of a 403 // if they need to login. if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) { return new Aphront403Response(); } } public function buildStandardPageView() { $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setController($this); return $view; } public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->appendChild($view); $response = new AphrontWebpageResponse(); $response->setContent($page->render()); return $response; } public function getApplicationURI($path = '') { if (!$this->getCurrentApplication()) { throw new Exception("No application!"); } return $this->getCurrentApplication()->getApplicationURI($path); } public function buildApplicationPage($view, array $options) { $page = $this->buildStandardPageView(); $title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ? 'Phabricator' : pht('Bacon Ice Cream for Breakfast'); $application = $this->getCurrentApplication(); $page->setTitle(idx($options, 'title', $title)); if ($application) { $page->setApplicationName($application->getName()); if ($application->getTitleGlyph()) { $page->setGlyph($application->getTitleGlyph()); } } if (!($view instanceof AphrontSideNavFilterView)) { $nav = new AphrontSideNavFilterView(); $nav->appendChild($view); $view = $nav; } $user = $this->getRequest()->getUser(); $view->setUser($user); $page->appendChild($view); $object_phids = idx($options, 'pageObjects', array()); if ($object_phids) { $page->appendPageObjects($object_phids); foreach ($object_phids as $object_phid) { PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $object_phid); } } if (idx($options, 'device')) { $page->setDeviceReady(true); } $page->setShowChrome(idx($options, 'chrome', true)); $application_menu = $this->buildApplicationMenu(); if ($application_menu) { $page->setApplicationMenu($application_menu); } $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } public function didProcessRequest($response) { $request = $this->getRequest(); $response->setRequest($request); $seen = array(); while ($response instanceof AphrontProxyResponse) { $hash = spl_object_hash($response); if (isset($seen[$hash])) { $seen[] = get_class($response); throw new Exception( "Cycle while reducing proxy responses: ". implode(' -> ', $seen)); } $seen[$hash] = get_class($response); $response = $response->reduceProxyResponse(); } if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->setController($this); - $view->appendChild(hsprintf( - '
%s
', + $view->appendChild(phutil_tag( + 'div', + array('style' => 'padding: 2em 0;'), $response->buildResponseString())); $page_response = new AphrontWebpageResponse(); $page_response->setContent($view->render()); $page_response->setHTTPResponseCode($response->getHTTPResponseCode()); return $page_response; } else { $response->getDialog()->setIsStandalone(true); return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } return $response; } protected function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Attempting to access handle which wasn't loaded: {$phid}"); } return $this->handles[$phid]; } protected function loadHandles(array $phids) { $phids = array_filter($phids); $this->handles = $this->loadViewerHandles($phids); return $this; } protected function getLoadedHandles() { return $this->handles; } protected function loadViewerHandles(array $phids) { return id(new PhabricatorHandleQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs($phids) ->execute(); } /** * Render a list of links to handles, identified by PHIDs. The handles must * already be loaded. * * @param list List of PHIDs to render links to. * @param string Style, one of "\n" (to put each item on its own line) * or "," (to list items inline, separated by commas). * @return string Rendered list of handle links. */ protected function renderHandlesForPHIDs(array $phids, $style = "\n") { $style_map = array( "\n" => phutil_tag('br'), ',' => ', ', ); if (empty($style_map[$style])) { throw new Exception("Unknown handle list style '{$style}'!"); } return implode_selected_handle_links($style_map[$style], $this->getLoadedHandles(), array_filter($phids)); } protected function buildApplicationMenu() { return null; } protected function buildApplicationCrumbs() { $crumbs = array(); $application = $this->getCurrentApplication(); if ($application) { $sprite = $application->getIconName(); if (!$sprite) { $sprite = 'application'; } $crumbs[] = id(new PhabricatorCrumbView()) ->setHref($this->getApplicationURI()) ->setIcon($sprite); } $view = new PhabricatorCrumbsView(); foreach ($crumbs as $crumb) { $view->addCrumb($crumb); } return $view; } protected function hasApplicationCapability($capability) { return PhabricatorPolicyFilter::hasCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function requireApplicationCapability($capability) { PhabricatorPolicyFilter::requireCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function explainApplicationCapability( $capability, $positive_message, $negative_message) { $can_act = $this->hasApplicationCapability($capability); if ($can_act) { $message = $positive_message; $icon_name = 'enable-grey'; } else { $message = $negative_message; $icon_name = 'lock'; } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ->setSpriteIcon($icon_name); require_celerity_resource('policy-css'); $phid = $this->getCurrentApplication()->getPHID(); $explain_uri = "/policy/explain/{$phid}/{$capability}/"; $message = phutil_tag( 'div', array( 'class' => 'policy-capability-explanation', ), array( $icon, javelin_tag( 'a', array( 'href' => $explain_uri, 'sigil' => 'workflow', ), $message), )); return array($can_act, $message); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php index 5ab092868..88e2dfeb0 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php @@ -1,96 +1,96 @@ getRequest(); $user = $request->getUser(); $year_d = phabricator_format_local_time($now, $user, 'Y'); $year = $request->getInt('year', $year_d); $month_d = phabricator_format_local_time($now, $user, 'm'); $month = $request->getInt('month', $month_d); $day = phabricator_format_local_time($now, $user, 'j'); $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( 'day BETWEEN %s AND %s', "{$year}-{$month}-01", "{$year}-{$month}-31"); $statuses = id(new PhabricatorUserStatus()) ->loadAllWhere( 'dateTo >= %d AND dateFrom <= %d', strtotime("{$year}-{$month}-01"), strtotime("{$year}-{$month}-01 next month")); if ($month == $month_d && $year == $year_d) { $month_view = new AphrontCalendarMonthView($month, $year, $day); } else { $month_view = new AphrontCalendarMonthView($month, $year); } $month_view->setBrowseURI($request->getRequestURI()); $month_view->setUser($user); $month_view->setHolidays($holidays); $phids = mpull($statuses, 'getUserPHID'); $handles = $this->loadViewerHandles($phids); foreach ($statuses as $status) { $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $name_text = $handles[$status->getUserPHID()]->getName(); $status_text = $status->getHumanStatus(); $event->setUserPHID($status->getUserPHID()); $event->setName("{$name_text} ({$status_text})"); $details = ''; if ($status->getDescription()) { $details = "\n\n".rtrim($status->getDescription()); } $event->setDescription( $status->getTerseSummary($user).$details); $event->setEventID($status->getID()); $month_view->addEvent($event); } $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $nav->appendChild( array( $this->getNoticeView(), - hsprintf('
%s
', $month_view), + phutil_tag('div', array('style' => 'padding: 20px;'), $month_view), )); return $this->buildApplicationPage( $nav, array( 'title' => pht('Calendar'), 'device' => true, )); } private function getNoticeView() { $request = $this->getRequest(); $view = null; if ($request->getExists('created')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully created your status.')); } else if ($request->getExists('updated')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully updated your status.')); } else if ($request->getExists('deleted')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully deleted your status.')); } return $view; } } diff --git a/src/applications/calendar/view/AphrontCalendarMonthView.php b/src/applications/calendar/view/AphrontCalendarMonthView.php index 3597857a8..0859e0acf 100644 --- a/src/applications/calendar/view/AphrontCalendarMonthView.php +++ b/src/applications/calendar/view/AphrontCalendarMonthView.php @@ -1,327 +1,333 @@ browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function setHolidays(array $holidays) { assert_instances_of($holidays, 'PhabricatorCalendarHoliday'); $this->holidays = mpull($holidays, null, 'getDay'); return $this; } public function __construct($month, $year, $day = null) { $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { if (empty($this->user)) { throw new Exception("Call setUser() before render()!"); } $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); require_celerity_resource('aphront-calendar-view-css'); $first = reset($days); $empty = $first->format('w'); $markup = array(); $empty_box = phutil_tag( 'div', array('class' => 'aphront-calendar-day aphront-calendar-empty'), ''); for ($ii = 0; $ii < $empty; $ii++) { $markup[] = $empty_box; } $show_events = array(); foreach ($days as $day) { $day_number = $day->format('j'); $holiday = idx($this->holidays, $day->format('Y-m-d')); $class = 'aphront-calendar-day'; $weekday = $day->format('w'); if ($day_number == $this->day) { $class .= ' aphront-calendar-today'; } if ($holiday || $weekday == 0 || $weekday == 6) { $class .= ' aphront-calendar-not-work-day'; } $day->setTime(0, 0, 0); $epoch_start = $day->format('U'); $day->modify('+1 day'); $epoch_end = $day->format('U'); if ($weekday == 0) { $show_events = array(); } else { $show_events = array_fill_keys( array_keys($show_events), - hsprintf( - '
'. - ' '. - '
')); + phutil_tag_div( + 'aphront-calendar-event aphront-calendar-event-empty', + "\xC2\xA0")); //   } foreach ($events as $event) { if ($event->getEpochStart() >= $epoch_end) { // This list is sorted, so we can stop looking. break; } if ($event->getEpochStart() < $epoch_end && $event->getEpochEnd() > $epoch_start) { $show_events[$event->getUserPHID()] = $this->renderEvent( $event, $epoch_start, $epoch_end); } } $holiday_markup = null; if ($holiday) { $name = $holiday->getName(); $holiday_markup = phutil_tag( 'div', array( 'class' => 'aphront-calendar-holiday', 'title' => $name, ), $name); } - $markup[] = hsprintf( - '
'. - '
%s
'. - '%s%s'. - '
', + $markup[] = phutil_tag_div( $class, - $day_number, - $holiday_markup, - phutil_implode_html("\n", $show_events)); + array( + phutil_tag_div('aphront-calendar-date-number', $day_number), + $holiday_markup, + phutil_implode_html("\n", $show_events), + )); } $table = array(); $rows = array_chunk($markup, 7); foreach ($rows as $row) { - $table[] = hsprintf(''); + $cells = array(); while (count($row) < 7) { $row[] = $empty_box; } foreach ($row as $cell) { - $table[] = phutil_tag('td', array(), $cell); + $cells[] = phutil_tag('td', array(), $cell); } - $table[] = hsprintf(''); + $table[] = phutil_tag('tr', array(), $cells); } - $table = hsprintf( - ''. - '%s'. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '%s'. - '
SunMonTueWedThuFriSat
', - $this->renderCalendarHeader($first), - phutil_implode_html("\n", $table)); + + $header = phutil_tag( + 'tr', + array('class' => 'aphront-calendar-day-of-week-header'), + array( + phutil_tag('th', array(), pht('Sun')), + phutil_tag('th', array(), pht('Mon')), + phutil_tag('th', array(), pht('Tue')), + phutil_tag('th', array(), pht('Wed')), + phutil_tag('th', array(), pht('Thu')), + phutil_tag('th', array(), pht('Fri')), + phutil_tag('th', array(), pht('Sat')), + )); + + $table = phutil_tag( + 'table', + array('class' => 'aphront-calendar-view'), + array( + $this->renderCalendarHeader($first), + $header, + phutil_implode_html("\n", $table), + )); return $table; } private function renderCalendarHeader(DateTime $date) { $colspan = 7; $left_th = ''; $right_th = ''; // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { $colspan = 5; $uri = new PhutilURI($uri); list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); $query = array('year' => $prev_year, 'month' => $prev_month); $prev_link = phutil_tag( 'a', array('href' => (string) $uri->setQueryParams($query)), "\xE2\x86\x90"); list($next_year, $next_month) = $this->getNextYearAndMonth(); $query = array('year' => $next_year, 'month' => $next_month); $next_link = phutil_tag( 'a', array('href' => (string) $uri->setQueryParams($query)), "\xE2\x86\x92"); $left_th = phutil_tag('th', array(), $prev_link); $right_th = phutil_tag('th', array(), $next_link); } - return hsprintf( - '%s%s%s', - $left_th, - phutil_tag('th', array('colspan' => $colspan), $date->format('F Y')), - $right_th); + return phutil_tag( + 'tr', + array('class' => 'aphront-calendar-month-year-header'), + array( + $left_th, + phutil_tag('th', array('colspan' => $colspan), $date->format('F Y')), + $right_th, + )); } private function getNextYearAndMonth() { $month = $this->month; $year = $this->year; $next_year = $year; $next_month = $month + 1; if ($next_month == 13) { $next_year = $year + 1; $next_month = 1; } return array($next_year, $next_month); } private function getPrevYearAndMonth() { $month = $this->month; $year = $this->year; $prev_year = $year; $prev_month = $month - 1; if ($prev_month == 0) { $prev_year = $year - 1; $prev_month = 12; } return array($prev_year, $prev_month); } /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. * * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; // Get the year and month numbers of the following month, so we can // determine when this month ends. list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); $end_epoch = $end_date->format('U'); $days = array(); for ($day = 1; $day <= 31; $day++) { $day_date = new DateTime("{$year}-{$month}-{$day}", $timezone); $day_epoch = $day_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { $days[] = $day_date; } } return $days; } private function renderEvent( AphrontCalendarEventView $event, $epoch_start, $epoch_end) { $user = $this->user; $event_start = $event->getEpochStart(); $event_end = $event->getEpochEnd(); $classes = array(); $when = array(); $classes[] = 'aphront-calendar-event'; if ($event_start < $epoch_start) { $classes[] = 'aphront-calendar-event-continues-before'; $when[] = 'Started '.phabricator_datetime($event_start, $user); } else { $when[] = 'Starts at '.phabricator_time($event_start, $user); } if ($event_end > $epoch_end) { $classes[] = 'aphront-calendar-event-continues-after'; $when[] = 'Ends '.phabricator_datetime($event_end, $user); } else { $when[] = 'Ends at '.phabricator_time($event_end, $user); } Javelin::initBehavior('phabricator-tooltips'); $info = $event->getName(); if ($event->getDescription()) { $info .= "\n\n".$event->getDescription(); } if ($user->getPHID() == $event->getUserPHID()) { $tag = 'a'; $href = '/calendar/status/edit/'.$event->getEventID().'/'; } else { $tag = 'div'; $href = null; } $text_div = javelin_tag( $tag, array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $info."\n\n".implode("\n", $when), 'size' => 240, ), 'class' => 'aphront-calendar-event-text', 'href' => $href, ), phutil_utf8_shorten($event->getName(), 32)); return javelin_tag( 'div', array( 'class' => implode(' ', $classes), ), $text_div); } } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index 837674996..ce6b6a02c 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -1,502 +1,505 @@ 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->getIsDisabled()) { return array( 'ERR-USER-DISABLED', 'User is disabled.'); } if (PhabricatorUserEmail::isEmailVerificationRequired()) { $email = $user->loadPrimaryEmail(); if (!$email) { return array( 'ERR-USER-NOEMAIL', 'User has no primary email address.'); } if (!$email->getIsVerified()) { return array( 'ERR-USER-UNVERIFIED', 'User has unverified email address.'); } } $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'))); 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 = hsprintf('
%s
', $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/query/PhabricatorConduitSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php index 91a0ad7e4..43bf22bd3 100644 --- a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php @@ -1,137 +1,138 @@ setParameter('isStable', $request->getStr('isStable')); $saved->setParameter('isUnstable', $request->getStr('isUnstable')); $saved->setParameter('isDeprecated', $request->getStr('isDeprecated')); $saved->setParameter( 'applicationNames', $request->getStrList('applicationNames')); $saved->setParameter('nameContains', $request->getStr('nameContains')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorConduitMethodQuery()); $query->withIsStable($saved->getParameter('isStable')); $query->withIsUnstable($saved->getParameter('isUnstable')); $query->withIsDeprecated($saved->getParameter('isDeprecated')); $names = $saved->getParameter('applicationNames', array()); if ($names) { $query->withApplicationNames($names); } $contains = $saved->getParameter('nameContains'); if (strlen($contains)) { $query->withNameContains($contains); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name Contains') ->setName('nameContains') ->setValue($saved->getParameter('nameContains'))); $names = $saved->getParameter('applicationNames', array()); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Applications') ->setName('applicationNames') ->setValue(implode(', ', $names)) - ->setCaption( - pht('Example: %s', hsprintf('differential, paste')))); + ->setCaption(pht( + 'Example: %s', + phutil_tag('tt', array(), 'differential, paste')))); $is_stable = $saved->getParameter('isStable'); $is_unstable = $saved->getParameter('isUnstable'); $is_deprecated = $saved->getParameter('isDeprecated'); $form ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel('Stability') ->addCheckbox( 'isStable', 1, hsprintf( '%s: %s', pht('Stable Methods'), pht('Show established API methods with stable interfaces.')), $is_stable) ->addCheckbox( 'isUnstable', 1, hsprintf( '%s: %s', pht('Unstable Methods'), pht('Show new methods which are subject to change.')), $is_unstable) ->addCheckbox( 'isDeprecated', 1, hsprintf( '%s: %s', pht('Deprecated Methods'), pht( 'Show old methods which will be deleted in a future '. 'version of Phabricator.')), $is_deprecated)); } protected function getURI($path) { return '/conduit/'.$path; } public function getBuiltinQueryNames() { $names = array( 'modern' => pht('Modern Methods'), 'all' => pht('All Methods'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'modern': return $query ->setParameter('isStable', true) ->setParameter('isUnstable', true); case 'all': return $query ->setParameter('isStable', true) ->setParameter('isUnstable', true) ->setParameter('isDeprecated', true); } return parent::buildSavedQueryFromBuiltin($query_key); } } diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index e2da019e9..7ec6e41c7 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -1,545 +1,545 @@ 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())); if ($group) { $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($group->getName()) ->setHref($group_uri)); } $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($this->key) ->setHref('/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[] = hsprintf( - '%s%s', - pht('Example'), - pht('Value')); + $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[] = hsprintf( - '%s%s', - $description, - $value); + $table[] = phutil_tag('tr', array(), array( + phutil_tag('th', array(), $description), + phutil_tag('th', 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[] = hsprintf( - '%s%s', - pht('Source'), - pht('Value')); + $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[] = hsprintf( - '%s%s', - $source->getName(), - $value); + $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/conpherence/controller/ConpherenceNotificationPanelController.php b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php index 76c5897e6..1da1521bd 100644 --- a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php +++ b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php @@ -1,106 +1,106 @@ getRequest(); $user = $request->getUser(); $conpherences = array(); $unread_status = ConpherenceParticipationStatus::BEHIND; $participant_data = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->setLimit(5) ->execute(); if ($participant_data) { $conpherences = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withPHIDs(array_keys($participant_data)) ->needParticipantCache(true) ->execute(); } if ($conpherences) { require_celerity_resource('conpherence-notification-css'); // re-order the conpherences based on participation data $conpherences = array_select_keys( $conpherences, array_keys($participant_data)); $view = new AphrontNullView(); foreach ($conpherences as $conpherence) { $p_data = $participant_data[$conpherence->getPHID()]; $d_data = $conpherence->getDisplayData($user); $classes = array( 'phabricator-notification', 'conpherence-notification', ); if ($p_data->getParticipationStatus() == $unread_status) { $classes[] = 'phabricator-notification-unread'; } $uri = $this->getApplicationURI($conpherence->getID().'/'); $title = $d_data['title']; $subtitle = $d_data['subtitle']; $unread_count = $d_data['unread_count']; $epoch = $d_data['epoch']; $image = $d_data['image']; $msg_view = id(new ConpherenceMenuItemView()) ->setUser($user) ->setTitle($title) ->setSubtitle($subtitle) ->setHref($uri) ->setEpoch($epoch) ->setImageURI($image) ->setUnreadCount($unread_count); $view->appendChild(javelin_tag( 'div', array( 'class' => implode(' ', $classes), 'sigil' => 'notification', 'meta' => array( 'href' => $uri, ), ), $msg_view)); } $content = $view->render(); } else { - $content = hsprintf( - '
%s
', + $content = phutil_tag_div( + 'phabricator-notification no-notifications', pht('You have no messages.')); } $content = hsprintf( '
%s
'. '%s'. '
%s
', pht('Messages'), $content, phutil_tag( 'a', array( 'href' => '/conpherence/', ), 'View All Conpherences')); $unread = id(new ConpherenceParticipantCountQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->withParticipationStatus($unread_status) ->execute(); $unread_count = idx($unread, $user->getPHID(), 0); $json = array( 'content' => $content, 'number' => (int)$unread_count, ); return id(new AphrontAjaxResponse())->setContent($json); } } diff --git a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php index e241aac4f..d82cebaa5 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php @@ -1,54 +1,54 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $countdown = id(new PhabricatorCountdownQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$countdown) { return new Aphront404Response(); } if ($request->isFormPost()) { $countdown->delete(); return id(new AphrontRedirectResponse()) ->setURI('/countdown/'); } $inst = pht('Are you sure you want to delete the countdown %s?', $countdown->getTitle()); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle(pht('Really delete this countdown?')); - $dialog->appendChild(hsprintf('

%s

', $inst)); + $dialog->appendChild(phutil_tag('p', array(), $inst)); $dialog->addSubmitButton(pht('Delete')); $dialog->addCancelButton('/countdown/'); $dialog->setSubmitURI($request->getPath()); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/countdown/view/PhabricatorCountdownView.php b/src/applications/countdown/view/PhabricatorCountdownView.php index 9c09144b5..1f4b96114 100644 --- a/src/applications/countdown/view/PhabricatorCountdownView.php +++ b/src/applications/countdown/view/PhabricatorCountdownView.php @@ -1,78 +1,79 @@ headless = $headless; return $this; } public function setCountdown(PhabricatorCountdown $countdown) { $this->countdown = $countdown; return $this; } public function getTagContent() { $countdown = $this->countdown; require_celerity_resource('phabricator-countdown-css'); $header = null; if (!$this->headless) { $header = phutil_tag( 'div', array( 'class' => 'phabricator-timer-header', ), array( "C".$countdown->getID(), ' ', phutil_tag( 'a', array( 'href' => '/countdown/'.$countdown->getID(), ), $countdown->getTitle()), )); } - $container = celerity_generate_unique_node_id(); - $content = hsprintf( - '
- %s - - - - - - - - %s%s%s%s -
%s%s%s%s
-
', - $container, - $header, - pht('Days'), - pht('Hours'), - pht('Minutes'), - pht('Seconds'), + $ths = array( + phutil_tag('th', array(), pht('Days')), + phutil_tag('th', array(), pht('Hours')), + phutil_tag('th', array(), pht('Minutes')), + phutil_tag('th', array(), pht('Seconds')), + ); + + $dashes = array( javelin_tag('td', array('sigil' => 'phabricator-timer-days'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-hours'), '-'), javelin_tag('td', array('sigil' => 'phabricator-timer-minutes'), '-'), - javelin_tag('td', array('sigil' => 'phabricator-timer-seconds'), '-')); + javelin_tag('td', array('sigil' => 'phabricator-timer-seconds'), '-'), + ); + + $container = celerity_generate_unique_node_id(); + $content = phutil_tag( + 'div', + array('class' => 'phabricator-timer', 'id' => $container), + array( + $header, + phutil_tag('table', array('class' => 'phabricator-timer-table'), array( + phutil_tag('tr', array(), $ths), + phutil_tag('tr', array(), $dashes), + )), + )); Javelin::initBehavior('countdown-timer', array( 'timestamp' => $countdown->getEpoch(), 'container' => $container, )); return $content; } } diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php index e55cb30af..f6543a93d 100644 --- a/src/applications/differential/controller/DifferentialDiffViewController.php +++ b/src/applications/differential/controller/DifferentialDiffViewController.php @@ -1,166 +1,167 @@ 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[] = hsprintf( - '', + $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()))); 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/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php index fad8b48e0..b42c50c8c 100644 --- a/src/applications/differential/controller/DifferentialRevisionLandController.php +++ b/src/applications/differential/controller/DifferentialRevisionLandController.php @@ -1,130 +1,130 @@ revisionID = $data['id']; $this->strategyClass = $data['strategy']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $revision_id = $this->revisionID; $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($viewer) ->executeOne(); if (!$revision) { return new Aphront404Response(); } if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) { $this->pushStrategy = newv($this->strategyClass, array()); } else { throw new Exception( "Strategy type must be a valid class name and must subclass ". "DifferentialLandingStrategy. ". "'{$this->strategyClass}' is not a subclass of ". "DifferentialLandingStrategy."); } if ($request->isDialogFormPost()) { try { $this->attemptLand($revision, $request); $title = pht("Success!"); $text = pht("Revision was successfully landed."); } catch (Exception $ex) { $title = pht("Failed to land revision"); $text = 'moo'; if ($ex instanceof PhutilProxyException) { $text = hsprintf( '%s:
%s
', $ex->getMessage(), $ex->getPreviousException()->getMessage()); } else { - $text = hsprintf('
%s
', $ex->getMessage()); + $text = phutil_tag('pre', array(), $ex->getMessage()); } $text = id(new AphrontErrorView()) ->appendChild($text); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild(phutil_tag('p', array(), $text)) ->setSubmitURI('/D'.$revision_id) ->addSubmitButton(pht('Done')); return id(new AphrontDialogResponse())->setDialog($dialog); } $prompt = hsprintf('%s

%s', pht( 'This will squash and rebase revision %s, and push it to '. 'the default / master branch.', $revision_id), pht('It is an experimental feature and may not work.')); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht("Land Revision %s?", $revision_id)) ->appendChild($prompt) ->setSubmitURI($request->getRequestURI()) ->addSubmitButton(pht('Land it!')) ->addCancelButton('/D'.$revision_id); return id(new AphrontDialogResponse())->setDialog($dialog); } private function attemptLand($revision, $request) { $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { throw new Exception("Only Accepted revisions can be landed."); } $repository = $revision->getRepository(); if ($repository === null) { throw new Exception("revision is not attached to a repository."); } $can_push = PhabricatorPolicyFilter::hasCapability( $request->getUser(), $repository, DiffusionCapabilityPush::CAPABILITY); if (!$can_push) { throw new Exception( pht('You do not have permission to push to this repository.')); } $lock = $this->lockRepository($repository); try { $this->pushStrategy->processLandRequest( $request, $revision, $repository); } catch (Exception $e) { $lock->unlock(); throw $e; } $lock->unlock(); } private function lockRepository($repository) { $lock_name = __CLASS__.':'.($repository->getCallsign()); $lock = PhabricatorGlobalLock::newLock($lock_name); $lock->lock(); return $lock; } } diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index b965b78fc..bf2aeadde 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -1,409 +1,401 @@ getChangeset(); $change = $changeset->getChangeType(); $file = $changeset->getFileType(); $message = null; if ($change == DifferentialChangeType::TYPE_CHANGE && $file == DifferentialChangeType::FILE_TEXT) { if ($force) { // We have to force something to render because there were no changes // of other kinds. $message = pht('This file was not modified.'); } else { // Default case of changes to a text file, no metadata. return null; } } else { $none = hsprintf(''); switch ($change) { case DifferentialChangeType::TYPE_ADD: switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was added.', $none); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was added.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht( 'This directory was added.', $none); break; case DifferentialChangeType::FILE_BINARY: $message = pht( 'This binary file was added.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This symlink was added.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht( 'This submodule was added.', $none); break; } break; case DifferentialChangeType::TYPE_DELETE: switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was deleted.', $none); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was deleted.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht( 'This directory was deleted.', $none); break; case DifferentialChangeType::FILE_BINARY: $message = pht( 'This binary file was deleted.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht( 'This symlink was deleted.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht( 'This submodule was deleted.', $none); break; } break; case DifferentialChangeType::TYPE_MOVE_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was moved from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was moved from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht('This directory was moved from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $message = pht('This binary file was moved from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This symlink was moved from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht('This submodule was moved from %s.', $from); break; } break; case DifferentialChangeType::TYPE_COPY_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was copied from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was copied from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht('This directory was copied from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $message = pht('This binary file was copied from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This symlink was copied from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht('This submodule was copied from %s.', $from); break; } break; case DifferentialChangeType::TYPE_MOVE_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was moved to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht('This directory was moved to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $message = pht('This binary file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This symlink was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht('This submodule was moved to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_COPY_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This image was copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht('This directory was copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $message = pht('This binary file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This symlink was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht('This submodule was copied to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_MULTICOPY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht( 'This file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $message = pht( 'This image was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht( 'This directory was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $message = pht( 'This binary file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht( 'This symlink was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht( 'This submodule was deleted after being copied to %s.', $paths); break; } break; default: switch ($file) { case DifferentialChangeType::FILE_TEXT: $message = pht('This is a file.'); break; case DifferentialChangeType::FILE_IMAGE: $message = pht('This is an image.'); break; case DifferentialChangeType::FILE_DIRECTORY: $message = pht('This is a directory.'); break; case DifferentialChangeType::FILE_BINARY: $message = pht('This is a binary file.'); break; case DifferentialChangeType::FILE_SYMLINK: $message = pht('This is a symlink.'); break; case DifferentialChangeType::FILE_SUBMODULE: $message = pht('This is a submodule.'); break; } break; } } - return hsprintf( - '
%s
', - $message); + return phutil_tag_div('differential-meta-notice', $message); } protected function renderPropertyChangeHeader() { $changeset = $this->getChangeset(); $old = $changeset->getOldProperties(); $new = $changeset->getNewProperties(); $keys = array_keys($old + $new); sort($keys); $rows = array(); foreach ($keys as $key) { $oval = idx($old, $key); $nval = idx($new, $key); if ($oval !== $nval) { if ($oval === null) { $oval = phutil_tag('em', array(), 'null'); } else { $oval = phutil_escape_html_newlines($oval); } if ($nval === null) { $nval = phutil_tag('em', array(), 'null'); } else { $nval = phutil_escape_html_newlines($nval); } - $rows[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '', - $key, - $oval, - $nval); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('th', array(), $key), + phutil_tag('td', array('class' => 'oval'), $oval), + phutil_tag('td', array('class' => 'nval'), $nval), + )); } } - array_unshift($rows, hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '', - pht('Property Changes'), - pht('Old Value'), - pht('New Value'))); + array_unshift( + $rows, + phutil_tag('tr', array('class' => 'property-table-header'), array( + phutil_tag('th', array(), pht('Property Changes')), + phutil_tag('td', array('class' => 'oval'), pht('Old Value')), + phutil_tag('td', array('class' => 'nval'), pht('New Value')), + ))); return phutil_tag( 'table', array('class' => 'differential-property-table'), $rows); } public function renderShield($message, $force = 'default') { $end = count($this->getOldLines()); $reference = $this->getRenderingReference(); if ($force !== 'text' && $force !== 'whitespace' && $force !== 'none' && $force !== 'default') { throw new Exception("Invalid 'force' parameter '{$force}'!"); } $range = "0-{$end}"; if ($force == 'text') { // If we're forcing text, force the whole file to be rendered. $range = "{$range}/0-{$end}"; } $meta = array( 'ref' => $reference, 'range' => $range, ); if ($force == 'whitespace') { $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; } $content = array(); $content[] = $message; if ($force !== 'none') { $content[] = ' '; $content[] = javelin_tag( 'a', array( 'mustcapture' => true, 'sigil' => 'show-more', 'class' => 'complete', 'href' => '#', 'meta' => $meta, ), pht('Show File Contents')); } return $this->wrapChangeInTable( javelin_tag( 'tr', array( 'sigil' => 'context-target', ), phutil_tag( 'td', array( 'class' => 'differential-shield', 'colspan' => 6, ), $content))); } protected function wrapChangeInTable($content) { if (!$content) { return null; } return javelin_tag( 'table', array( 'class' => 'differential-diff remarkup-code PhabricatorMonospaced', 'sigil' => 'differential-diff', ), $content); } protected function renderInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { return $this->buildInlineComment($comment, $on_right)->render(); } protected function buildInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { $user = $this->getUser(); $edit = $user && ($comment->getAuthorPHID() == $user->getPHID()) && ($comment->isDraft()); $allow_reply = (bool)$user; return id(new DifferentialInlineCommentView()) ->setInlineComment($comment) ->setOnRight($on_right) ->setHandles($this->getHandles()) ->setMarkupEngine($this->getMarkupEngine()) ->setEditable($edit) ->setAllowReply($allow_reply); } } diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index 567116db0..25d8aeee5 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -1,77 +1,77 @@ buildPrimitives($range_start, $range_len); $out = array(); foreach ($primitives as $p) { $type = $p['type']; switch ($type) { case 'old': case 'new': $out[] = hsprintf(''); if ($type == 'old') { if ($p['htype']) { $class = 'left old'; } else { $class = 'left'; } - $out[] = hsprintf('%s', $p['line']); - $out[] = hsprintf(''); - $out[] = hsprintf('%s', $class, $p['render']); + $out[] = phutil_tag('th', array(), $p['line']); + $out[] = phutil_tag('th', array()); + $out[] = phutil_tag('td', array('class' => $class), $p['render']); } else if ($type == 'new') { if ($p['htype']) { $class = 'right new'; - $out[] = hsprintf(''); + $out[] = phutil_tag('th', array()); } else { $class = 'right'; - $out[] = hsprintf('%s', $p['oline']); + $out[] = phutil_tag('th', array(), $p['oline']); } - $out[] = hsprintf('%s', $p['line']); - $out[] = hsprintf('%s', $class, $p['render']); + $out[] = phutil_tag('th', array(), $p['line']); + $out[] = phutil_tag('td', array('class' => $class), $p['render']); } $out[] = hsprintf(''); break; case 'inline': $out[] = hsprintf(''); $out[] = hsprintf(''); $inline = $this->buildInlineComment( $p['comment'], $p['right']); $inline->setBuildScaffolding(false); $out[] = $inline->render(); $out[] = hsprintf(''); break; default: $out[] = hsprintf('%s', $type); break; } } if ($out) { return $this->wrapChangeInTable(phutil_implode_html('', $out)); } return null; } public function renderFileChange($old_file = null, $new_file = null, $id = 0, $vs = 0) { throw new Exception("Not implemented!"); } } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 378d4bdcc..f44889ce5 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -1,455 +1,448 @@ getHunkStartLines(); $context_not_available = null; if ($hunk_starts) { $context_not_available = javelin_tag( 'tr', array( 'sigil' => 'context-target', ), phutil_tag( 'td', array( 'colspan' => 6, 'class' => 'show-more' ), pht('Context not available.'))); } $html = array(); $old_lines = $this->getOldLines(); $new_lines = $this->getNewLines(); $gaps = $this->getGaps(); $reference = $this->getRenderingReference(); $left_id = $this->getOldChangesetID(); $right_id = $this->getNewChangesetID(); // "N" stands for 'new' and means the comment should attach to the new file // when stored, i.e. DifferentialInlineComment->setIsNewFile(). // "O" stands for 'old' and means the comment should attach to the old file. $left_char = $this->getOldAttachesToNewFile() ? 'N' : 'O'; $right_char = $this->getNewAttachesToNewFile() ? 'N' : 'O'; $changeset = $this->getChangeset(); $copy_lines = idx($changeset->getMetadata(), 'copy:lines', array()); $highlight_old = $this->getHighlightOld(); $highlight_new = $this->getHighlightNew(); $old_render = $this->getOldRender(); $new_render = $this->getNewRender(); $original_left = $this->getOriginalOld(); $original_right = $this->getOriginalNew(); $depths = $this->getDepths(); $mask = $this->getMask(); for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { // If we aren't going to show this line, we've just entered a gap. // Pop information about the next gap off the $gaps stack and render // an appropriate "Show more context" element. This branch eventually // increments $ii by the entire size of the gap and then continues // the loop. $gap = array_pop($gaps); $top = $gap[0]; $len = $gap[1]; $end = $top + $len - 20; $contents = array(); if ($len > 40) { $is_first_block = false; if ($ii == 0) { $is_first_block = true; } $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'ref' => $reference, 'range' => "{$top}-{$len}/{$top}-20", ), ), $is_first_block ? pht("Show First 20 Lines") : pht("\xE2\x96\xB2 Show 20 Lines")); } $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'type' => 'all', 'ref' => $reference, 'range' => "{$top}-{$len}/{$top}-{$len}", ), ), pht('Show All %d Lines', $len)); $is_last_block = false; if ($ii + $len >= $rows) { $is_last_block = true; } if ($len > 40) { $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'ref' => $reference, 'range' => "{$top}-{$len}/{$end}-20", ), ), $is_last_block ? pht("Show Last 20 Lines") : pht("\xE2\x96\xBC Show 20 Lines")); } $context = null; $context_line = null; if (!$is_last_block && $depths[$ii + $len]) { for ($l = $ii + $len - 1; $l >= $ii; $l--) { $line = $new_lines[$l]['text']; if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') { $context = $new_render[$l]; $context_line = $new_lines[$l]['line']; break; } } } $container = javelin_tag( 'tr', array( 'sigil' => 'context-target', ), array( phutil_tag( 'td', array( 'colspan' => 2, 'class' => 'show-more', ), phutil_implode_html( " \xE2\x80\xA2 ", // Bullet $contents)), phutil_tag( 'th', array( 'class' => 'show-context-line', ), $context_line ? (int)$context_line : null), phutil_tag( 'td', array( 'colspan' => 3, 'class' => 'show-context', ), // TODO: [HTML] Escaping model here isn't ideal. phutil_safe_html($context)), )); $html[] = $container; $ii += ($len - 1); continue; } $o_num = null; $o_classes = 'left'; $o_text = null; if (isset($old_lines[$ii])) { $o_num = $old_lines[$ii]['line']; $o_text = isset($old_render[$ii]) ? $old_render[$ii] : null; if ($old_lines[$ii]['type']) { if ($old_lines[$ii]['type'] == '\\') { $o_text = $old_lines[$ii]['text']; $o_classes .= ' comment'; } else if ($original_left && !isset($highlight_old[$o_num])) { $o_classes .= ' old-rebase'; } else if (empty($new_lines[$ii])) { $o_classes .= ' old old-full'; } else { $o_classes .= ' old'; } } } $n_copy = hsprintf(''); $n_cov = null; $n_colspan = 2; $n_classes = ''; $n_num = null; $n_text = null; if (isset($new_lines[$ii])) { $n_num = $new_lines[$ii]['line']; $n_text = isset($new_render[$ii]) ? $new_render[$ii] : null; $coverage = $this->getCodeCoverage(); if ($coverage !== null) { if (empty($coverage[$n_num - 1])) { $cov_class = 'N'; } else { $cov_class = $coverage[$n_num - 1]; } $cov_class = 'cov-'.$cov_class; - $n_cov = hsprintf('', $cov_class); + $n_cov = phutil_tag('td', array('class' => "cov {$cov_class}")); $n_colspan--; } if ($new_lines[$ii]['type']) { if ($new_lines[$ii]['type'] == '\\') { $n_text = $new_lines[$ii]['text']; $n_class = 'comment'; } else if ($original_right && !isset($highlight_new[$n_num])) { $n_class = 'new-rebase'; } else if (empty($old_lines[$ii])) { $n_class = 'new new-full'; } else { $n_class = 'new'; } $n_classes = $n_class; if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) { - $n_copy = hsprintf('', $n_class); + $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); } else { list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num]; $title = ($orig_type == '-' ? 'Moved' : 'Copied').' from '; if ($orig_file == '') { $title .= "line {$orig_line}"; } else { $title .= basename($orig_file). ":{$orig_line} in dir ". dirname('/'.$orig_file); } $class = ($orig_type == '-' ? 'new-move' : 'new-copy'); $n_copy = javelin_tag( 'td', array( 'meta' => array( 'msg' => $title, ), 'class' => 'copy '.$class, ), ''); } } } $n_classes .= ' right'.$n_colspan; if (isset($hunk_starts[$o_num])) { $html[] = $context_not_available; } if ($o_num && $left_id) { $o_id = 'C'.$left_id.$left_char.'L'.$o_num; } else { $o_id = null; } if ($n_num && $right_id) { $n_id = 'C'.$right_id.$right_char.'L'.$n_num; } else { $n_id = null; } + // NOTE: This is a unicode zero-width space, which we use as a hint + // when intercepting 'copy' events to make sure sensible text ends + // up on the clipboard. See the 'phabricator-oncopy' behavior. + $zero_space = "\xE2\x80\x8B"; + // NOTE: The Javascript is sensitive to whitespace changes in this // block! - $html[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '%s'. - // NOTE: This is a unicode zero-width space, which we use as a hint - // when intercepting 'copy' events to make sure sensible text ends - // up on the clipboard. See the 'phabricator-oncopy' behavior. - ''. - "\xE2\x80\x8B%s". - ''. - '%s'. - '', + $html[] = phutil_tag('tr', array(), array( phutil_tag('th', array('id' => $o_id), $o_num), - $o_classes, $o_text, + phutil_tag('td', array('class' => $o_classes), $o_text), phutil_tag('th', array('id' => $n_id), $n_num), $n_copy, - $n_classes, $n_colspan, $n_text, - $n_cov); + phutil_tag( + 'td', + array('class' => $n_classes, 'colspan' => $n_colspan), + array($zero_space, $n_text)), + $n_cov, + )); if ($context_not_available && ($ii == $rows - 1)) { $html[] = $context_not_available; } $old_comments = $this->getOldComments(); $new_comments = $this->getNewComments(); if ($o_num && isset($old_comments[$o_num])) { foreach ($old_comments[$o_num] as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = false); $new = ''; if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $key => $new_comment) { if ($comment->isCompatible($new_comment)) { $new = $this->renderInlineComment($new_comment, $on_right = true); unset($new_comments[$n_num][$key]); } } } - $html[] = hsprintf( - ''. - ''. - '%s'. - ''. - '%s'. - '', - $comment_html, - $new); + $html[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $comment_html), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3, 'class' => 'right3'), $new), + )); } } if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); - $html[] = hsprintf( - ''. - ''. - ''. - ''. - '%s'. - '', - $comment_html); + $html[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left')), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $comment_html), + )); } } } return $this->wrapChangeInTable(phutil_implode_html('', $html)); } public function renderFileChange($old_file = null, $new_file = null, $id = 0, $vs = 0) { $old = null; if ($old_file) { $old = phutil_tag( 'div', array( 'class' => 'differential-image-stage' ), phutil_tag( 'img', array( 'src' => $old_file->getBestURI(), ))); } $new = null; if ($new_file) { $new = phutil_tag( 'div', array( 'class' => 'differential-image-stage' ), phutil_tag( 'img', array( 'src' => $new_file->getBestURI(), ))); } $html_old = array(); $html_new = array(); foreach ($this->getOldComments() as $on_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = false); - $html_old[] = hsprintf( - ''. - ''. - '%s'. - ''. - ''. - '', - $comment_html); + $html_old[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $comment_html), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3, 'class' => 'right3')), + )); } } foreach ($this->getNewComments() as $lin_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); - $html_new[] = hsprintf( - ''. - ''. - ''. - ''. - '%s'. - '', - $comment_html); + $html_new[] = phutil_tag('tr', array('class' => 'inline'), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left')), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $comment_html), + )); } } if (!$old) { - $th_old = hsprintf(''); + $th_old = phutil_tag('th', array()); } else { - $th_old = hsprintf('1', $vs); + $th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1); } if (!$new) { - $th_new = hsprintf(''); + $th_new = phutil_tag('th', array()); } else { - $th_new = hsprintf('1', $id); + $th_new = phutil_tag('th', array('id' => "C{$id}OL1"), 1); } $output = hsprintf( ''. '%s'. '%s'. '%s'. '%s'. ''. '%s'. '%s', $th_old, $old, $th_new, $new, phutil_implode_html('', $html_old), phutil_implode_html('', $html_new)); $output = $this->wrapChangeInTable($output); return $this->renderChangesetTable($output); } } diff --git a/src/applications/differential/view/DifferentialAddCommentView.php b/src/applications/differential/view/DifferentialAddCommentView.php index ceba6a219..822a85d6a 100644 --- a/src/applications/differential/view/DifferentialAddCommentView.php +++ b/src/applications/differential/view/DifferentialAddCommentView.php @@ -1,204 +1,205 @@ revision = $revision; return $this; } public function setAuxFields(array $aux_fields) { assert_instances_of($aux_fields, 'DifferentialFieldSpecification'); $this->auxFields = $aux_fields; return $this; } public function setActions(array $actions) { $this->actions = $actions; return $this; } public function setActionURI($uri) { $this->actionURI = $uri; return $this; } public function setDraft(PhabricatorDraft $draft = null) { $this->draft = $draft; return $this; } public function setReviewers(array $names) { $this->reviewers = $names; return $this; } public function setCCs(array $names) { $this->ccs = $names; return $this; } public function render() { require_celerity_resource('differential-revision-add-comment-css'); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $revision = $this->revision; $action = null; if ($this->draft) { $action = idx($this->draft->getMetadata(), 'action'); } $enable_reviewers = DifferentialAction::allowReviewers($action); $enable_ccs = ($action == DifferentialAction::ACTION_ADDCCS); $add_reviewers_labels = array( 'add_reviewers' => pht('Add Reviewers'), 'request_review' => pht('Add Reviewers'), 'resign' => pht('Suggest Reviewers'), ); $form = new AphrontFormView(); $form ->setWorkflow(true) ->setUser($this->user) ->setAction($this->actionURI) ->addHiddenInput('revision_id', $revision->getID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setValue($action) ->setID('comment-action') ->setOptions($this->actions)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel($enable_reviewers ? $add_reviewers_labels[$action] : $add_reviewers_labels['add_reviewers']) ->setName('reviewers') ->setControlID('add-reviewers') ->setControlStyle($enable_reviewers ? null : 'display: none') ->setID('add-reviewers-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add CCs')) ->setName('ccs') ->setControlID('add-ccs') ->setControlStyle($enable_ccs ? null : 'display: none') ->setID('add-ccs-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('comment') ->setID('comment-content') ->setLabel(pht('Comment')) ->setValue($this->draft ? $this->draft->getDraft() : null) ->setUser($this->user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($is_serious ? pht('Submit') : pht('Clowncopterize'))); Javelin::initBehavior( 'differential-add-reviewers-and-ccs', array( 'dynamic' => array( 'add-reviewers-tokenizer' => array( 'actions' => array( 'request_review' => 1, 'add_reviewers' => 1, 'resign' => 1, ), 'src' => '/typeahead/common/usersorprojects/', 'value' => $this->reviewers, 'row' => 'add-reviewers', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'labels' => $add_reviewers_labels, 'placeholder' => pht('Type a user or project name...'), ), 'add-ccs-tokenizer' => array( 'actions' => array('add_ccs' => 1), 'src' => '/typeahead/common/mailable/', 'value' => $this->ccs, 'row' => 'add-ccs', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a user or mailing list...'), ), ), 'select' => 'comment-action', )); $diff = $revision->loadActiveDiff(); $warnings = mpull($this->auxFields, 'renderWarningBoxForRevisionAccept'); Javelin::initBehavior( 'differential-accept-with-errors', array( 'select' => 'comment-action', 'warnings' => 'warnings', )); $rev_id = $revision->getID(); Javelin::initBehavior( 'differential-feedback-preview', array( 'uri' => '/differential/comment/preview/'.$rev_id.'/', 'preview' => 'comment-preview', 'action' => 'comment-action', 'content' => 'comment-content', 'previewTokenizers' => array( 'reviewers' => 'add-reviewers-tokenizer', 'ccs' => 'add-ccs-tokenizer', ), 'inlineuri' => '/differential/comment/inline/preview/'.$rev_id.'/', 'inline' => 'inline-comment-preview', )); $warning_container = array(); foreach ($warnings as $warning) { if ($warning) { $warning_container[] = $warning->render(); } } $header = id(new PHUIHeaderView()) ->setHeader($is_serious ? pht('Add Comment') : pht('Leap Into Action')); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('comment') ->setNavigationMarker(true); $warn = phutil_tag('div', array('id' => 'warnings'), $warning_container); - $preview = hsprintf( - '
'. - '
'. - '%s'. - '
'. - '
'. - '
'. - '
', + $loading = phutil_tag( + 'span', + array('class' => 'aphront-panel-preview-loading-text'), pht('Loading comment preview...')); + $preview = phutil_tag_div( + 'aphront-panel-preview aphront-panel-flush', + array( + phutil_tag('div', array('id' => 'comment-preview'), $loading), + phutil_tag('div', array('id' => 'inline-comment-preview')), + )); $comment_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($anchor) ->appendChild($warn) ->appendChild($form); return array($comment_box, $preview); } } diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index ca05bf97b..17ed42eac 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -1,346 +1,348 @@ title = $title; return $this; } private function getTitle() { return $this->title; } public function setBranch($branch) { $this->branch = $branch; return $this; } private function getBranch() { return $this->branch; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function setVisibleChangesets($visible_changesets) { $this->visibleChangesets = $visible_changesets; return $this; } public function setInlineCommentControllerURI($uri) { $this->inlineURI = $uri; return $this; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function setRenderingReferences(array $references) { $this->references = $references; return $this; } public function setSymbolIndexes(array $indexes) { $this->symbolIndexes = $indexes; return $this; } public function setRenderURI($render_uri) { $this->renderURI = $render_uri; return $this; } public function setWhitespace($whitespace) { $this->whitespace = $whitespace; return $this; } public function setVsMap(array $vs_map) { $this->vsMap = $vs_map; return $this; } public function getVsMap() { return $this->vsMap; } public function setStandaloneURI($uri) { $this->standaloneURI = $uri; return $this; } public function setRawFileURIs($l, $r) { $this->leftRawFileURI = $l; $this->rightRawFileURI = $r; return $this; } public function render() { require_celerity_resource('differential-changeset-view-css'); $changesets = $this->changesets; Javelin::initBehavior('differential-toggle-files', array( 'pht' => array( 'undo' => pht('Undo'), 'collapsed' => pht('This file content has been collapsed.')) )); Javelin::initBehavior( 'differential-dropdown-menus', array( 'pht' => array( 'Open in Editor' => pht('Open in Editor'), 'Show Entire File' => pht('Show Entire File'), 'Entire File Shown' => pht('Entire File Shown'), "Can't Toggle Unloaded File" => pht("Can't Toggle Unloaded File"), 'Expand File' => pht('Expand File'), 'Collapse File' => pht('Collapse File'), 'Browse in Diffusion' => pht('Browse in Diffusion'), 'View Standalone' => pht('View Standalone'), 'Show Raw File (Left)' => pht('Show Raw File (Left)'), 'Show Raw File (Right)' => pht('Show Raw File (Right)'), 'Configure Editor' => pht('Configure Editor'), ), )); $output = array(); $mapping = array(); foreach ($changesets as $key => $changeset) { $file = $changeset->getFilename(); $class = 'differential-changeset'; if (!$this->inlineURI) { $class .= ' differential-changeset-noneditable'; } $ref = $this->references[$key]; $detail = new DifferentialChangesetDetailView(); $view_options = $this->renderViewOptionsDropdown( $detail, $ref, $changeset); $detail->setChangeset($changeset); $detail->addButton($view_options); $detail->setSymbolIndex(idx($this->symbolIndexes, $key)); $detail->setVsChangesetID(idx($this->vsMap, $changeset->getID())); $detail->setEditable(true); $uniq_id = 'diff-'.$changeset->getAnchorName(); if (isset($this->visibleChangesets[$key])) { $load = 'Loading...'; $mapping[$uniq_id] = $ref; } else { $load = javelin_tag( 'a', array( 'href' => '#'.$uniq_id, 'meta' => array( 'id' => $uniq_id, 'ref' => $ref, 'kill' => true, ), 'sigil' => 'differential-load', 'mustcapture' => true, ), pht('Load')); } $detail->appendChild( phutil_tag( 'div', array( 'id' => $uniq_id, ), phutil_tag('div', array('class' => 'differential-loading'), $load))); $output[] = $detail->render(); } require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('differential-populate', array( 'registry' => $mapping, 'whitespace' => $this->whitespace, 'uri' => $this->renderURI, )); Javelin::initBehavior('differential-show-more', array( 'uri' => $this->renderURI, 'whitespace' => $this->whitespace, )); Javelin::initBehavior('differential-comment-jump', array()); if ($this->inlineURI) { $undo_templates = $this->renderUndoTemplates(); Javelin::initBehavior('differential-edit-inline-comments', array( 'uri' => $this->inlineURI, 'undo_templates' => $undo_templates, 'stage' => 'differential-review-stage', )); } $header = id(new PHUIHeaderView()) ->setHeader($this->getTitle()); $content = phutil_tag( 'div', array( 'class' => 'differential-review-stage', 'id' => 'differential-review-stage', ), $output); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($content); return $object_box; } /** * Render the "Undo" markup for the inline comment undo feature. */ private function renderUndoTemplates() { $link = javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'differential-inline-comment-undo', ), pht('Undo')); $div = phutil_tag( 'div', array( 'class' => 'differential-inline-undo', ), array('Changes discarded. ', $link)); return array( - 'l' => hsprintf( - ''. - ''. - ''. - '
%s
', - $div), - - 'r' => hsprintf( - ''. - ''. - ''. - '
%s
', - $div), + 'l' => phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array(), $div), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3)), + ))), + + 'r' => phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array()), + phutil_tag('th', array()), + phutil_tag('td', array('colspan' => 3), $div), + ))), ); } private function renderViewOptionsDropdown( DifferentialChangesetDetailView $detail, $ref, DifferentialChangeset $changeset) { $meta = array(); $qparams = array( 'ref' => $ref, 'whitespace' => $this->whitespace, ); if ($this->standaloneURI) { $uri = new PhutilURI($this->standaloneURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['standaloneURI'] = (string)$uri; } $repository = $this->repository; if ($repository) { try { $meta['diffusionURI'] = (string)$repository->getDiffusionBrowseURIForPath( $this->user, $changeset->getAbsoluteRepositoryPath($repository, $this->diff), idx($changeset->getMetadata(), 'line:first'), $this->getBranch()); } catch (DiffusionSetupException $e) { // Ignore } } $change = $changeset->getChangeType(); if ($this->leftRawFileURI) { if ($change != DifferentialChangeType::TYPE_ADD) { $uri = new PhutilURI($this->leftRawFileURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['leftURI'] = (string)$uri; } } if ($this->rightRawFileURI) { if ($change != DifferentialChangeType::TYPE_DELETE && $change != DifferentialChangeType::TYPE_MULTICOPY) { $uri = new PhutilURI($this->rightRawFileURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['rightURI'] = (string)$uri; } } $user = $this->user; if ($user && $repository) { $path = ltrim( $changeset->getAbsoluteRepositoryPath($repository, $this->diff), '/'); $line = idx($changeset->getMetadata(), 'line:first', 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); if ($editor_link) { $meta['editor'] = $editor_link; } else { $meta['editorConfigure'] = '/settings/panel/display/'; } } $meta['containerID'] = $detail->getID(); $caret = phutil_tag('span', array('class' => 'caret'), ''); return javelin_tag( 'a', array( 'class' => 'button grey small dropdown', 'meta' => $meta, 'href' => idx($meta, 'detailURI', '#'), 'target' => '_blank', 'sigil' => 'differential-view-options', ), array(pht('View Options'), $caret)); } } diff --git a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php index 1ba389839..84fae4224 100644 --- a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php +++ b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php @@ -1,312 +1,309 @@ changesets = $changesets; return $this; } public function setVisibleChangesets($visible_changesets) { $this->visibleChangesets = $visible_changesets; return $this; } public function setRenderingReferences(array $references) { $this->references = $references; return $this; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function setUnitTestData($unit_test_data) { $this->unitTestData = $unit_test_data; return $this; } public function setRevisionID($revision_id) { $this->revisionID = $revision_id; return $this; } public function setWhitespace($whitespace) { $this->whitespace = $whitespace; return $this; } public function render() { require_celerity_resource('differential-core-view-css'); require_celerity_resource('differential-table-of-contents-css'); $rows = array(); $coverage = array(); if ($this->unitTestData) { $coverage_by_file = array(); foreach ($this->unitTestData as $result) { $test_coverage = idx($result, 'coverage'); if (!$test_coverage) { continue; } foreach ($test_coverage as $file => $results) { $coverage_by_file[$file][] = $results; } } foreach ($coverage_by_file as $file => $coverages) { $coverage[$file] = ArcanistUnitTestResult::mergeCoverage($coverages); } } $changesets = $this->changesets; $paths = array(); foreach ($changesets as $id => $changeset) { $type = $changeset->getChangeType(); $ftype = $changeset->getFileType(); $ref = idx($this->references, $id); $display_file = $changeset->getDisplayFilename(); $meta = null; if (DifferentialChangeType::isOldLocationChangeType($type)) { $away = $changeset->getAwayPaths(); if (count($away) > 1) { $meta = array(); if ($type == DifferentialChangeType::TYPE_MULTICOPY) { $meta[] = pht('Deleted after being copied to multiple locations:'); } else { $meta[] = pht('Copied to multiple locations:'); } foreach ($away as $path) { $meta[] = $path; } $meta = phutil_implode_html(phutil_tag('br'), $meta); } else { if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) { $display_file = $this->renderRename( $display_file, reset($away), "\xE2\x86\x92"); } else { $meta = pht('Copied to %s', reset($away)); } } } else if ($type == DifferentialChangeType::TYPE_MOVE_HERE) { $old_file = $changeset->getOldFile(); $display_file = $this->renderRename( $display_file, $old_file, "\xE2\x86\x90"); } else if ($type == DifferentialChangeType::TYPE_COPY_HERE) { $meta = pht('Copied from %s', $changeset->getOldFile()); } $link = $this->renderChangesetLink($changeset, $ref, $display_file); $line_count = $changeset->getAffectedLineCount(); if ($line_count == 0) { $lines = null; } else { $lines = ' '.pht('(%d line(s))', $line_count); } $char = DifferentialChangeType::getSummaryCharacterForChangeType($type); $chartitle = DifferentialChangeType::getFullNameForChangeType($type); $desc = DifferentialChangeType::getShortNameForFileType($ftype); if ($desc) { $desc = '('.$desc.')'; } $pchar = ($changeset->getOldProperties() === $changeset->getNewProperties()) ? null - : hsprintf('M', pht('Properties Changed')); + : phutil_tag('span', array('title' => pht('Properties Changed')), 'M') + ; $fname = $changeset->getFilename(); $cov = $this->renderCoverage($coverage, $fname); if ($cov === null) { $mcov = $cov = phutil_tag('em', array(), '-'); } else { $mcov = phutil_tag( 'div', array( 'id' => 'differential-mcoverage-'.md5($fname), 'class' => 'differential-mcoverage-loading', ), (isset($this->visibleChangesets[$id]) ? 'Loading...' : '?')); } - $rows[] = hsprintf( - ''. - '%s'. - '%s'. - '%s'. - '%s%s'. - '%s'. - '%s'. - '', - $chartitle, $char, - $pchar, - $desc, - $link, $lines, - $cov, - $mcov); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag( + 'td', + array('class' => 'differential-toc-char', 'title' => $chartitle), + $char), + phutil_tag('td', array('class' => 'differential-toc-prop'), $pchar), + phutil_tag('td', array('class' => 'differential-toc-ftype'), $desc), + phutil_tag( + 'td', + array('class' => 'differential-toc-file'), + array($link, $lines)), + phutil_tag('td', array('class' => 'differential-toc-cov'), $cov), + phutil_tag('td', array('class' => 'differential-toc-mcov'), $mcov), + )); if ($meta) { - $rows[] = hsprintf( - ''. - ''. - '%s'. - '', - $meta); + $rows[] = phutil_tag('tr', array(), array( + phutil_tag('td', array('colspan' => 3)), + phutil_tag('td', array('class' => 'differential-toc-meta'), $meta), + )); } if ($this->diff && $this->repository) { $paths[] = $changeset->getAbsoluteRepositoryPath($this->repository, $this->diff); } } $editor_link = null; if ($paths && $this->user) { $editor_link = $this->user->loadEditorLink( $paths, 1, // line number $this->repository->getCallsign()); if ($editor_link) { $editor_link = phutil_tag( 'a', array( 'href' => $editor_link, 'class' => 'button differential-toc-edit-all', ), pht('Open All in Editor')); } } $reveal_link = javelin_tag( 'a', array( 'sigil' => 'differential-reveal-all', 'mustcapture' => true, 'class' => 'button differential-toc-reveal-all', ), pht('Show All Context')); - $buttons = hsprintf( - '%s%s', - $editor_link, - $reveal_link); + $buttons = phutil_tag('tr', array(), + phutil_tag('td', array('colspan' => 7), + array($editor_link, $reveal_link))); $content = hsprintf( '%s'. '
'. ''. ''. ''. ''. ''. ''. ''. ''. ''. '%s%s'. '
Path%s%s
'. '
', id(new PhabricatorAnchorView()) ->setAnchorName('toc') ->setNavigationMarker(true) ->render(), pht('Coverage (All)'), pht('Coverage (Touched)'), phutil_implode_html("\n", $rows), $buttons); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Table of Contents')) ->appendChild($content); } private function renderRename($display_file, $other_file, $arrow) { $old = explode('/', $display_file); $new = explode('/', $other_file); $start = count($old); foreach ($old as $index => $part) { if (!isset($new[$index]) || $part != $new[$index]) { $start = $index; break; } } $end = count($old); foreach (array_reverse($old) as $from_end => $part) { $index = count($new) - $from_end - 1; if (!isset($new[$index]) || $part != $new[$index]) { $end = $from_end; break; } } $rename = '{'. implode('/', array_slice($old, $start, count($old) - $end - $start)). ' '.$arrow.' '. implode('/', array_slice($new, $start, count($new) - $end - $start)). '}'; array_splice($new, $start, count($new) - $end - $start, $rename); return implode('/', $new); } private function renderCoverage(array $coverage, $file) { $info = idx($coverage, $file); if (!$info) { return null; } $not_covered = substr_count($info, 'U'); $covered = substr_count($info, 'C'); if (!$not_covered && !$covered) { return null; } return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); } private function renderChangesetLink( DifferentialChangeset $changeset, $ref, $display_file) { return javelin_tag( 'a', array( 'href' => '#'.$changeset->getAnchorName(), 'meta' => array( 'id' => 'diff-'.$changeset->getAnchorName(), 'ref' => $ref, ), 'sigil' => 'differential-load', ), $display_file); } } diff --git a/src/applications/differential/view/DifferentialInlineCommentEditView.php b/src/applications/differential/view/DifferentialInlineCommentEditView.php index 672d945e4..a2318eb06 100644 --- a/src/applications/differential/view/DifferentialInlineCommentEditView.php +++ b/src/applications/differential/view/DifferentialInlineCommentEditView.php @@ -1,156 +1,160 @@ inputs[] = array($key, $value); return $this; } public function setSubmitURI($uri) { $this->uri = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function setOnRight($on_right) { $this->onRight = $on_right; $this->addHiddenInput('on_right', $on_right); return $this; } public function setNumber($number) { $this->number = $number; return $this; } public function setLength($length) { $this->length = $length; return $this; } public function render() { if (!$this->uri) { throw new Exception("Call setSubmitURI() before render()!"); } if (!$this->user) { throw new Exception("Call setUser() before render()!"); } $content = phabricator_form( $this->user, array( 'action' => $this->uri, 'method' => 'POST', 'sigil' => 'inline-edit-form', ), array( $this->renderInputs(), $this->renderBody(), )); - return hsprintf( - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '
%s%s
', - $this->onRight ? null : $content, - $this->onRight ? $content : null); + return phutil_tag('table', array(), phutil_tag( + 'tr', + array('class' => 'inline-comment-splint'), + array( + phutil_tag('th', array()), + phutil_tag( + 'td', + array('class' => 'left'), + $this->onRight ? null : $content), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $this->onRight ? $content : null), + ))); } private function renderInputs() { $out = array(); foreach ($this->inputs as $input) { list($name, $value) = $input; $out[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $name, 'value' => $value, )); } return $out; } private function renderBody() { $buttons = array(); $buttons[] = phutil_tag('button', array(), 'Ready'); $buttons[] = javelin_tag( 'button', array( 'sigil' => 'inline-edit-cancel', 'class' => 'grey', ), pht('Cancel')); $formatting = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'article/Remarkup_Reference.html'), 'tabindex' => '-1', 'target' => '_blank', ), pht('Formatting Reference')); $title = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-title', ), $this->title); $body = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-body', ), $this->renderChildren()); $edit = phutil_tag( 'div', array( 'class' => 'differential-inline-comment-edit-buttons', ), array( $formatting, $buttons, phutil_tag('div', array('style' => 'clear: both'), ''), )); return javelin_tag( 'div', array( 'class' => 'differential-inline-comment-edit', 'sigil' => 'differential-inline-comment', 'meta' => array( 'on_right' => $this->onRight, 'number' => $this->number, 'length' => $this->length, ), ), array( $title, $body, $edit, )); } } diff --git a/src/applications/differential/view/DifferentialInlineCommentView.php b/src/applications/differential/view/DifferentialInlineCommentView.php index f9ff40e2b..0b491b64e 100644 --- a/src/applications/differential/view/DifferentialInlineCommentView.php +++ b/src/applications/differential/view/DifferentialInlineCommentView.php @@ -1,264 +1,269 @@ inlineComment = $comment; return $this; } public function setOnRight($on_right) { $this->onRight = $on_right; return $this; } public function setBuildScaffolding($scaffold) { $this->buildScaffolding = $scaffold; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setEditable($editable) { $this->editable = $editable; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setAllowReply($allow_reply) { $this->allowReply = $allow_reply; return $this; } public function render() { $inline = $this->inlineComment; $start = $inline->getLineNumber(); $length = $inline->getLineLength(); if ($length) { $end = $start + $length; $line = 'Lines '.number_format($start).'-'.number_format($end); } else { $line = 'Line '.number_format($start); } $metadata = array( 'id' => $inline->getID(), 'number' => $inline->getLineNumber(), 'length' => $inline->getLineLength(), 'on_right' => $this->onRight, 'original' => $inline->getContent(), ); $sigil = 'differential-inline-comment'; if ($this->preview) { $sigil = $sigil . ' differential-inline-comment-preview'; } $content = $inline->getContent(); $handles = $this->handles; $links = array(); $is_synthetic = false; if ($inline->getSyntheticAuthor()) { $is_synthetic = true; } $is_draft = false; if ($inline->isDraft() && !$is_synthetic) { $links[] = pht('Not Submitted Yet'); $is_draft = true; } if (!$this->preview) { $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-prev', ), pht('Previous')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-next', ), pht('Next')); if ($this->allowReply) { if (!$is_synthetic) { // NOTE: No product reason why you can't reply to these, but the reply // mechanism currently sends the inline comment ID to the server, not // file/line information, and synthetic comments don't have an inline // comment ID. $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-reply', ), pht('Reply')); } } } $anchor_name = 'inline-'.$inline->getID(); if ($this->editable && !$this->preview) { $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-edit', ), pht('Edit')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-delete', ), pht('Delete')); } else if ($this->preview) { $links[] = javelin_tag( 'a', array( 'meta' => array( 'anchor' => $anchor_name, ), 'sigil' => 'differential-inline-preview-jump', ), pht('Not Visible')); $links[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'differential-inline-delete', ), pht('Delete')); } if ($links) { $links = phutil_tag( 'span', array('class' => 'differential-inline-comment-links'), phutil_implode_html(" \xC2\xB7 ", $links)); } else { $links = null; } $content = $this->markupEngine->getOutput( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); if ($this->preview) { $anchor = null; } else { $anchor = phutil_tag( 'a', array( 'name' => $anchor_name, 'id' => $anchor_name, 'class' => 'differential-inline-comment-anchor', ), ''); } $classes = array( 'differential-inline-comment', ); if ($is_draft) { $classes[] = 'differential-inline-comment-unsaved-draft'; } if ($is_synthetic) { $classes[] = 'differential-inline-comment-synthetic'; } $classes = implode(' ', $classes); if ($is_synthetic) { $author = $inline->getSyntheticAuthor(); } else { $author = $handles[$inline->getAuthorPHID()]->getName(); } + $line = phutil_tag( + 'span', + array('class' => 'differential-inline-comment-line'), + $line); + $markup = javelin_tag( 'div', array( 'class' => $classes, 'sigil' => $sigil, 'meta' => $metadata, ), - hsprintf( - '
'. - '%s%s %s %s'. - '
'. - '
'. - '
%s
'. - '
', - $anchor, - $links, - $line, - $author, - $content)); + array( + phutil_tag_div('differential-inline-comment-head', array( + $anchor, + $links, + ' ', + $line, + ' ', + $author, + )), + phutil_tag_div( + 'differential-inline-comment-content', + phutil_tag_div('phabricator-remarkup', $content)), + )); return $this->scaffoldMarkup($markup); } private function scaffoldMarkup($markup) { if (!$this->buildScaffolding) { return $markup; } $left_markup = !$this->onRight ? $markup : ''; $right_markup = $this->onRight ? $markup : ''; - return hsprintf( - ''. - ''. - ''. - ''. - ''. - ''. - ''. - '
%s%s
', - $left_markup, - $right_markup); + return phutil_tag('table', array(), + phutil_tag('tr', array(), array( + phutil_tag('th', array()), + phutil_tag('td', array('class' => 'left'), $left_markup), + phutil_tag('th', array()), + phutil_tag( + 'td', + array('colspan' => 3, 'class' => 'right3'), + $right_markup), + ))); } } diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index aec57bfe8..432180713 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -1,147 +1,145 @@ localCommits = $local_commits; return $this; } public function render() { $user = $this->user; if (!$user) { throw new Exception("Call setUser() before render()-ing this view."); } $local = $this->localCommits; if (!$local) { return null; } require_celerity_resource('differential-local-commits-view-css'); $has_tree = false; $has_local = false; foreach ($local as $commit) { if (idx($commit, 'tree')) { $has_tree = true; } if (idx($commit, 'local')) { $has_local = true; } } $rows = array(); $highlight = true; foreach ($local as $commit) { if ($highlight) { $class = 'alt'; $highlight = false; } else { $class = ''; $highlight = true; } $row = array(); if (idx($commit, 'commit')) { $commit_hash = self::formatCommit($commit['commit']); } else if (isset($commit['rev'])) { $commit_hash = self::formatCommit($commit['rev']); } else { $commit_hash = null; } $row[] = phutil_tag('td', array(), $commit_hash); if ($has_tree) { $tree = idx($commit, 'tree'); $tree = self::formatCommit($tree); $row[] = phutil_tag('td', array(), $tree); } if ($has_local) { $local_rev = idx($commit, 'local', null); $row[] = phutil_tag('td', array(), $local_rev); } $parents = idx($commit, 'parents', array()); foreach ($parents as $k => $parent) { if (is_array($parent)) { $parent = idx($parent, 'rev'); } $parents[$k] = self::formatCommit($parent); } $parents = phutil_implode_html(phutil_tag('br'), $parents); $row[] = phutil_tag('td', array(), $parents); $author = nonempty( idx($commit, 'user'), idx($commit, 'author')); $row[] = phutil_tag('td', array(), $author); $message = idx($commit, 'message'); $summary = idx($commit, 'summary'); $summary = phutil_utf8_shorten($summary, 80); $view = new AphrontMoreView(); $view->setSome($summary); if ($message && (trim($summary) != trim($message))) { $view->setMore(phutil_escape_html_newlines($message)); } $row[] = phutil_tag( 'td', array( 'class' => 'summary', ), $view->render()); $date = nonempty( idx($commit, 'date'), idx($commit, 'time')); if ($date) { $date = phabricator_datetime($date, $user); } $row[] = phutil_tag('td', array(), $date); $rows[] = phutil_tag('tr', array('class' => $class), $row); } $headers = array(); $headers[] = phutil_tag('th', array(), pht('Commit')); if ($has_tree) { $headers[] = phutil_tag('th', array(), pht('Tree')); } if ($has_local) { $headers[] = phutil_tag('th', array(), pht('Local')); } $headers[] = phutil_tag('th', array(), pht('Parents')); $headers[] = phutil_tag('th', array(), pht('Author')); $headers[] = phutil_tag('th', array(), pht('Summary')); $headers[] = phutil_tag('th', array(), pht('Date')); $headers = phutil_tag('tr', array(), $headers); - $content = hsprintf( - '
'. - '%s%s
'. - '
', - $headers, - phutil_implode_html("\n", $rows)); + $content = phutil_tag_div('differential-panel', phutil_tag( + 'table', + array('class' => 'differential-local-commits-table'), + array($headers, phutil_implode_html("\n", $rows)))); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Local Commits')) ->appendChild($content); } private static function formatCommit($commit) { return substr($commit, 0, 12); } } diff --git a/src/applications/differential/view/DifferentialRevisionCommentView.php b/src/applications/differential/view/DifferentialRevisionCommentView.php index c7499ddcc..820954dff 100644 --- a/src/applications/differential/view/DifferentialRevisionCommentView.php +++ b/src/applications/differential/view/DifferentialRevisionCommentView.php @@ -1,305 +1,302 @@ comment = $comment; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { $this->markupEngine = $markup_engine; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlines = $inline_comments; return $this; } public function setChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); // Ship these in sorted by getSortKey() and keyed by ID... or else! $this->changesets = $changesets; return $this; } public function setTargetDiff($target) { $this->target = $target; return $this; } public function setVersusDiffID($diff_vs) { $this->versusDiffID = $diff_vs; return $this; } public function setAnchorName($anchor_name) { $this->anchorName = $anchor_name; return $this; } public function render() { if (!$this->user) { throw new Exception("Call setUser() before rendering!"); } require_celerity_resource('phabricator-remarkup-css'); require_celerity_resource('differential-revision-comment-css'); $comment = $this->comment; $action = $comment->getAction(); $action_class = 'differential-comment-action-'.$action; $info = array(); $content = $comment->getContent(); $hide_comments = true; if (strlen(rtrim($content))) { $hide_comments = false; $content = $this->markupEngine->getOutput( $comment, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); - $content = hsprintf( - '
%s
', - $content); + $content = phutil_tag_div('phabricator-remarkup', $content); } $inline_render = $this->renderInlineComments(); if ($inline_render) { $hide_comments = false; } $author = $this->handles[$comment->getAuthorPHID()]; $author_link = $author->renderLink(); $metadata = $comment->getMetadata(); $added_reviewers = idx( $metadata, DifferentialComment::METADATA_ADDED_REVIEWERS, array()); $removed_reviewers = idx( $metadata, DifferentialComment::METADATA_REMOVED_REVIEWERS, array()); $added_ccs = idx( $metadata, DifferentialComment::METADATA_ADDED_CCS, array()); $verb = DifferentialAction::getActionPastTenseVerb($comment->getAction()); $actions = array(); // TODO: i18n switch ($comment->getAction()) { case DifferentialAction::ACTION_ADDCCS: $actions[] = hsprintf( "%s added CCs: %s.", $author_link, $this->renderHandleList($added_ccs)); $added_ccs = null; break; case DifferentialAction::ACTION_ADDREVIEWERS: $actions[] = hsprintf( "%s added reviewers: %s.", $author_link, $this->renderHandleList($added_reviewers)); $added_reviewers = null; break; case DifferentialAction::ACTION_UPDATE: $diff_id = idx($metadata, DifferentialComment::METADATA_DIFF_ID); if ($diff_id) { $diff_link = phutil_tag( 'a', array( 'href' => '/D'.$comment->getRevisionID().'?id='.$diff_id, ), 'Diff #'.$diff_id); $actions[] = hsprintf( "%s updated this revision to %s.", $author_link, $diff_link); } else { $actions[] = hsprintf( "%s %s this revision.", $author_link, $verb); } break; default: $actions[] = hsprintf( "%s %s this revision.", $author_link, $verb); break; } if ($added_reviewers) { $actions[] = hsprintf( "%s added reviewers: %s.", $author_link, $this->renderHandleList($added_reviewers)); } if ($removed_reviewers) { $actions[] = hsprintf( "%s removed reviewers: %s.", $author_link, $this->renderHandleList($removed_reviewers)); } if ($added_ccs) { $actions[] = hsprintf( "%s added CCs: %s.", $author_link, $this->renderHandleList($added_ccs)); } foreach ($actions as $key => $action) { $actions[$key] = phutil_tag('div', array(), $action); } $xaction_view = id(new PhabricatorTransactionView()) ->setUser($this->user) ->setImageURI($author->getImageURI()) ->setContentSource($comment->getContentSource()) ->addClass($action_class) ->setActions($actions); if ($this->preview) { $xaction_view->setIsPreview($this->preview); } else { $xaction_view->setEpoch($comment->getDateCreated()); if ($this->anchorName) { $anchor_text = 'D'.$comment->getRevisionID(). '#'.preg_replace('/^comment-/', '', $this->anchorName); $xaction_view->setAnchor($this->anchorName, $anchor_text); } } if (!$hide_comments) { - $xaction_view->appendChild(hsprintf( - '
%s%s
', - $content, - $inline_render)); + $xaction_view->appendChild(phutil_tag_div( + 'differential-comment-core', + array($content, $inline_render))); } return $xaction_view->render(); } private function renderHandleList(array $phids) { $result = array(); foreach ($phids as $phid) { $result[] = $this->handles[$phid]->renderLink(); } return phutil_implode_html(', ', $result); } private function renderInlineComments() { if (!$this->inlines) { return null; } $inlines = $this->inlines; $changesets = $this->changesets; $inlines_by_changeset = mgroup($inlines, 'getChangesetID'); $inlines_by_changeset = array_select_keys( $inlines_by_changeset, array_keys($this->changesets)); $view = new PhabricatorInlineSummaryView(); foreach ($inlines_by_changeset as $changeset_id => $inlines) { $changeset = $changesets[$changeset_id]; $items = array(); foreach ($inlines as $inline) { $on_target = ($this->target) && ($this->target->getID() == $changeset->getDiffID()); $is_visible = false; if ($inline->getIsNewFile()) { // This comment is on the right side of the versus diff, and visible // on the left side of the page. if ($this->versusDiffID) { if ($changeset->getDiffID() == $this->versusDiffID) { $is_visible = true; } } // This comment is on the right side of the target diff, and visible // on the right side of the page. if ($on_target) { $is_visible = true; } } else { // Ths comment is on the left side of the target diff, and visible // on the left side of the page. if (!$this->versusDiffID) { if ($on_target) { $is_visible = true; } } // TODO: We still get one edge case wrong here, when we have a // versus diff and the file didn't exist in the old version. The // comment is visible because we show the left side of the target // diff when there's no corresponding file in the versus diff, but // we incorrectly link it off-page. } $item = array( 'id' => $inline->getID(), 'line' => $inline->getLineNumber(), 'length' => $inline->getLineLength(), 'content' => $this->markupEngine->getOutput( $inline, DifferentialInlineComment::MARKUP_FIELD_BODY), ); if (!$is_visible) { $diff_id = $changeset->getDiffID(); $item['where'] = '(On Diff #'.$diff_id.')'; $item['href'] = 'D'.$this->comment->getRevisionID(). '?id='.$diff_id. '#inline-'.$inline->getID(); } $items[] = $item; } $view->addCommentGroup($changeset->getFilename(), $items); } return $view; } } diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index c10f985b1..e4a78860d 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -1,327 +1,329 @@ diffs = $diffs; return $this; } public function setSelectedVersusDiffID($id) { $this->selectedVersusDiffID = $id; return $this; } public function setSelectedDiffID($id) { $this->selectedDiffID = $id; return $this; } public function setSelectedWhitespace($whitespace) { $this->selectedWhitespace = $whitespace; return $this; } public function render() { require_celerity_resource('differential-core-view-css'); require_celerity_resource('differential-revision-history-css'); $data = array( array( 'name' => 'Base', 'id' => null, 'desc' => 'Base', 'age' => null, 'obj' => null, ), ); $seq = 0; foreach ($this->diffs as $diff) { $data[] = array( 'name' => 'Diff '.(++$seq), 'id' => $diff->getID(), 'desc' => $diff->getDescription(), 'age' => $diff->getDateCreated(), 'obj' => $diff, ); } $max_id = $diff->getID(); $idx = 0; $rows = array(); $disable = false; $radios = array(); $last_base = null; foreach ($data as $row) { $diff = $row['obj']; $name = $row['name']; $id = $row['id']; $old_class = null; $new_class = null; if ($id) { $new_checked = ($this->selectedDiffID == $id); $new = javelin_tag( 'input', array( 'type' => 'radio', 'name' => 'id', 'value' => $id, 'checked' => $new_checked ? 'checked' : null, 'sigil' => 'differential-new-radio', )); if ($new_checked) { $new_class = " revhistory-new-now"; $disable = true; } } else { $new = null; } if ($max_id != $id) { $uniq = celerity_generate_unique_node_id(); $old_checked = ($this->selectedVersusDiffID == $id); $old = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'vs', 'value' => $id, 'id' => $uniq, 'checked' => $old_checked ? 'checked' : null, 'disabled' => $disable ? 'disabled' : null, )); $radios[] = $uniq; if ($old_checked) { $old_class = " revhistory-old-now"; } } else { $old = null; } $desc = $row['desc']; if ($row['age']) { $age = phabricator_datetime($row['age'], $this->getUser()); } else { $age = null; } if (++$idx % 2) { $class = 'alt'; } else { $class = null; } $lint_attrs = array('class' => 'revhistory-star'); $unit_attrs = array('class' => 'revhistory-star'); if ($diff) { $lint = self::renderDiffLintStar($row['obj']); $unit = self::renderDiffUnitStar($row['obj']); $lint_attrs['title'] = self::getDiffLintMessage($diff); $unit_attrs['title'] = self::getDiffUnitMessage($diff); $base = $this->renderBaseRevision($diff); } else { $lint = null; $unit = null; $base = null; } if ($last_base !== null && $base !== $last_base) { // TODO: Render some kind of notice about rebases. } $last_base = $base; $id_link = phutil_tag( 'a', array('href' => '/differential/diff/'.$id.'/'), $id); $rows[] = phutil_tag( 'tr', array('class' => $class), array( phutil_tag('td', array('class' => 'revhistory-name'), $name), phutil_tag('td', array('class' => 'revhistory-id'), $id_link), phutil_tag('td', array('class' => 'revhistory-base'), $base), phutil_tag('td', array('class' => 'revhistory-desc'), $desc), phutil_tag('td', array('class' => 'revhistory-age'), $age), phutil_tag('td', $lint_attrs, $lint), phutil_tag('td', $unit_attrs, $unit), phutil_tag('td', array('class' => 'revhistory-old'.$old_class), $old), phutil_tag('td', array('class' => 'revhistory-new'.$new_class), $new), )); } Javelin::initBehavior( 'differential-diff-radios', array( 'radios' => $radios, )); $options = array( DifferentialChangesetParser::WHITESPACE_IGNORE_FORCE => 'Ignore All', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => 'Ignore Most', DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING => 'Ignore Trailing', DifferentialChangesetParser::WHITESPACE_SHOW_ALL => 'Show All', ); foreach ($options as $value => $label) { $options[$value] = phutil_tag( 'option', array( 'value' => $value, 'selected' => ($value == $this->selectedWhitespace) ? 'selected' : null, ), $label); } $select = phutil_tag('select', array('name' => 'whitespace'), $options); array_unshift($rows, phutil_tag('tr', array(), array( phutil_tag('th', array(), pht('Diff')), phutil_tag('th', array(), pht('ID')), phutil_tag('th', array(), pht('Base')), phutil_tag('th', array(), pht('Description')), phutil_tag('th', array(), pht('Created')), phutil_tag('th', array(), pht('Lint')), phutil_tag('th', array(), pht('Unit')), ))); - $content = hsprintf( - '
'. - '
'. - ''. - '%s'. - ''. - ''. - ''. - '
'. - ''. - ''. - '
'. - '
'. - '
', - phutil_implode_html("\n", $rows), - pht('Whitespace Changes: %s', $select), - pht('Show Diff')); + $label = pht('Whitespace Changes: %s', $select); + + $content = phutil_tag_div( + 'differential-revision-history differential-panel', + phutil_tag( + 'form', + array('action' => '#toc'), + phutil_tag( + 'table', + array('class' => 'differential-revision-history-table'), array( + phutil_implode_html("\n", $rows), + phutil_tag('tr', array(), phutil_tag( + 'td', + array('colspan' => 9, 'class' => 'diff-differ-submit'), + array( + phutil_tag('label', array(), $label), + phutil_tag('button', array(), pht('Show Diff')), + ))) + )))); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Update History')) ->appendChild($content); } const STAR_NONE = 'none'; const STAR_OKAY = 'okay'; const STAR_WARN = 'warn'; const STAR_FAIL = 'fail'; const STAR_SKIP = 'skip'; public static function renderDiffLintStar(DifferentialDiff $diff) { static $map = array( DifferentialLintStatus::LINT_NONE => self::STAR_NONE, DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY, DifferentialLintStatus::LINT_WARN => self::STAR_WARN, DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL, DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP, DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP ); $star = idx($map, $diff->getLintStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function renderDiffUnitStar(DifferentialDiff $diff) { static $map = array( DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE, DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY, DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN, DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL, DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP, DifferentialUnitStatus::UNIT_POSTPONED => self::STAR_SKIP, ); $star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function getDiffLintMessage(DifferentialDiff $diff) { switch ($diff->getLintStatus()) { case DifferentialLintStatus::LINT_NONE: return 'No Linters Available'; case DifferentialLintStatus::LINT_OKAY: return 'Lint OK'; case DifferentialLintStatus::LINT_WARN: return 'Lint Warnings'; case DifferentialLintStatus::LINT_FAIL: return 'Lint Errors'; case DifferentialLintStatus::LINT_SKIP: return 'Lint Skipped'; case DifferentialLintStatus::LINT_POSTPONED: return 'Lint Postponed'; } return '???'; } public static function getDiffUnitMessage(DifferentialDiff $diff) { switch ($diff->getUnitStatus()) { case DifferentialUnitStatus::UNIT_NONE: return 'No Unit Test Coverage'; case DifferentialUnitStatus::UNIT_OKAY: return 'Unit Tests OK'; case DifferentialUnitStatus::UNIT_WARN: return 'Unit Test Warnings'; case DifferentialUnitStatus::UNIT_FAIL: return 'Unit Test Errors'; case DifferentialUnitStatus::UNIT_SKIP: return 'Unit Tests Skipped'; case DifferentialUnitStatus::UNIT_POSTPONED: return 'Unit Tests Postponed'; } return '???'; } private static function renderDiffStar($star) { $class = 'diff-star-'.$star; return phutil_tag( 'span', array('class' => $class), "\xE2\x98\x85"); } private function renderBaseRevision(DifferentialDiff $diff) { switch ($diff->getSourceControlSystem()) { case 'git': $base = $diff->getSourceControlBaseRevision(); if (strpos($base, '@') === false) { return substr($base, 0, 7); } else { // The diff is from git-svn $base = explode('@', $base); $base = last($base); return $base; } case 'svn': $base = $diff->getSourceControlBaseRevision(); $base = explode('@', $base); $base = last($base); return $base; default: return null; } } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index d52bda931..067fe72a3 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1084 +1,1084 @@ getRequest()->getUser(); $drequest = DiffusionRequest::newFromDictionary($data); $this->diffusionRequest = $drequest; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $callsign = $drequest->getRepository()->getCallsign(); $content = array(); $repository = $drequest->getRepository(); $commit = $drequest->loadCommit(); $crumbs = $this->buildCrumbs(array( 'commit' => true, )); if (!$commit) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array('commit' => $drequest->getCommit())); if (!$exists) { return new Aphront404Response(); } $error = id(new AphrontErrorView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); return $this->buildApplicationPage( array( $crumbs, $error, ), array( 'title' => pht('Commit Still Parsing'), )); } $commit_data = $drequest->loadCommitData(); $commit->attachCommitData($commit_data); $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $audit_requests = id(new PhabricatorAuditQuery()) ->withCommitPHIDs(array($commit->getPHID())) ->execute(); $this->auditAuthorityPHIDs = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $changesets = null; if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new AphrontErrorView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING); $error_panel->appendChild( pht("This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); $content[] = $error_panel; $content[] = $top_anchor; } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $user); require_celerity_resource('diffusion-commit-view-css'); require_celerity_resource('phabricator-remarkup-css'); $parents = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array('commit' => $drequest->getCommit())); $headsup_view = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); $commit_properties = $this->loadCommitProperties( $commit, $commit_data, $parents, $audit_requests); $property_list = id(new PHUIPropertyListView()) ->setHasKeyboardShortcuts(true) ->setUser($user) ->setObject($commit); foreach ($commit_properties as $key => $value) { $property_list->addProperty($key, $value); } $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $property_list->invokeWillRenderEvent(); $property_list->setActionList($headsup_actions); $detail_list = new PHUIPropertyListView(); $detail_list->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); $content[] = $top_anchor; $object_box = id(new PHUIObjectBoxView()) ->setHeader($headsup_view) ->addPropertyList($property_list) ->addPropertyList($detail_list); $content[] = $object_box; } $content[] = $this->buildComments($commit); $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $content[] = $this->buildMergesTable($commit); // TODO: This is silly, but the logic to figure out which audits are // highlighted currently lives in PhabricatorAuditListView. Refactor this // to be less goofy. $highlighted_audits = id(new PhabricatorAuditListView()) ->setAudits($audit_requests) ->setAuthorityPHIDs($this->auditAuthorityPHIDs) ->setUser($user) ->setCommits(array($commit->getPHID() => $commit)) ->getHighlightedAudits(); $owners_paths = array(); if ($highlighted_audits) { $packages = id(new PhabricatorOwnersPackage())->loadAllWhere( 'phid IN (%Ls)', mpull($highlighted_audits, 'getAuditorPHID')); if ($packages) { $owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'repositoryPHID = %s AND packageID IN (%Ld)', $repository->getPHID(), mpull($packages, 'getID')); } } $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); $change_table->setOwnersPaths($owners_paths); $count = count($changes); $bad_commit = null; if ($count == 0) { $bad_commit = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE fullCommitName = %s', PhabricatorRepository::TABLE_BADCOMMIT, 'r'.$callsign.$commit->getCommitIdentifier()); } if ($bad_commit) { $content[] = $this->renderStatusMessage( pht('Bad Commit'), $bad_commit['description']); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $content[] = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $content[] = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $content[] = $this->renderStatusMessage( pht('Enormous Commit'), pht( 'This commit is enormous, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else { // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_panel = new AphrontPanelView(); $change_panel->setHeader("Changes (".number_format($count).")"); $change_panel->setID('toc'); if ($count > self::CHANGES_LIMIT && !$show_all_details) { $show_all_button = phutil_tag( 'a', array( 'class' => 'button green', 'href' => '?show_all=true', ), pht('Show All Changes')); $warning_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle('Very Large Commit') ->appendChild( pht("This commit is very large. Load each file individually.")); $change_panel->appendChild($warning_view); $change_panel->addButton($show_all_button); } $change_panel->appendChild($change_table); $change_panel->setNoBackground(); $content[] = $change_panel; $changesets = DiffusionPathChange::convertToDifferentialChangesets( $changes); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception("Unknown VCS."); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere( 'commitPHID = %s AND (auditCommentID IS NOT NULL OR authorPHID = %s)', $commit->getPHID(), $user->getPHID()); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/'); $change_list->setRepository($repository); $change_list->setUser($user); // pick the first branch for "Browse in Diffusion" View Option $branches = $commit_data->getCommitDetail('seenOnBranches', array()); $first_branch = reset($branches); $change_list->setBranch($first_branch); $change_list->setStandaloneURI( '/diffusion/'.$callsign.'/diff/'); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, '/diffusion/'.$callsign.'/diff/?view=r'); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); $change_references = array(); foreach ($changesets as $key => $changeset) { $change_references[$changeset->getID()] = $references[$key]; } $change_table->setRenderingReferences($change_references); $content[] = $change_list->render(); } $content[] = $this->renderAddCommentPanel($commit, $audit_requests); $commit_id = 'r'.$callsign.$commit->getCommitIdentifier(); $short_name = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $prefs = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; $pref_collapse = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED; $show_filetree = $prefs->getPreference($pref_filetree); $collapsed = $prefs->getPreference($pref_collapse); if ($changesets && $show_filetree) { $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle($short_name) ->setBaseURI(new PhutilURI('/'.$commit_id)) ->build($changesets) ->setCrumbs($crumbs) ->setCollapsed((bool)$collapsed) ->appendChild($content); $content = $nav; } else { $content = array($crumbs, $content); } return $this->buildApplicationPage( $content, array( 'title' => $commit_id, 'pageObjects' => array($commit->getPHID()), )); } private function loadCommitProperties( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents, array $audit_requests) { assert_instances_of($parents, 'PhabricatorRepositoryCommit'); $user = $this->getRequest()->getUser(); $commit_phid = $commit->getPHID(); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK]); $proj_phids = array_keys( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]); $revision_phid = key( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('authorPHID')) { $phids[] = $data->getCommitDetail('authorPHID'); } if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } if ($data->getCommitDetail('committerPHID')) { $phids[] = $data->getCommitDetail('committerPHID'); } if ($parents) { foreach ($parents as $parent) { $phids[] = $parent->getPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($commit->getAuditStatus()) { $status = PhabricatorAuditCommitStatusConstants::getStatusName( $commit->getAuditStatus()); $tag = id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setName($status); switch ($commit->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: $tag->setBackgroundColor(PhabricatorTagView::COLOR_ORANGE); break; case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: $tag->setBackgroundColor(PhabricatorTagView::COLOR_RED); break; case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: $tag->setBackgroundColor(PhabricatorTagView::COLOR_BLUE); break; case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: $tag->setBackgroundColor(PhabricatorTagView::COLOR_GREEN); break; } $props['Status'] = $tag; } if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $props['Auditors'] = $this->renderAuditStatusView( $user_requests); } if ($other_requests) { $props['Project/Package Auditors'] = $this->renderAuditStatusView( $other_requests); } } $props['Committed'] = phabricator_datetime($commit->getEpoch(), $user); $author_phid = $data->getCommitDetail('authorPHID'); if ($data->getCommitDetail('authorPHID')) { $props['Author'] = $handles[$author_phid]->renderLink(); } else { $props['Author'] = $data->getAuthorName(); } $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $props['Reviewer'] = $handles[$reviewer_phid]->renderLink(); } $committer = $data->getCommitDetail('committer'); if ($committer) { $committer_phid = $data->getCommitDetail('committerPHID'); if ($data->getCommitDetail('committerPHID')) { $props['Committer'] = $handles[$committer_phid]->renderLink(); } else { $props['Committer'] = $committer; } } if ($revision_phid) { $props['Differential Revision'] = $handles[$revision_phid]->renderLink(); } if ($parents) { $parent_links = array(); foreach ($parents as $parent) { $parent_links[] = $handles[$parent->getPHID()]->renderLink(); } $props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links); } $request = $this->getDiffusionRequest(); $props['Branches'] = phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown')); $props['Tags'] = phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown')); $callsign = $request->getRepository()->getCallsign(); $root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier(); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); $refs = $this->buildRefs($request); if ($refs) { $props['References'] = $refs; } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $props['Tasks'] = $task_list; } if ($proj_phids) { $proj_list = array(); foreach ($proj_phids as $phid) { $proj_list[] = $handles[$phid]->renderLink(); } $proj_list = phutil_implode_html(phutil_tag('br'), $proj_list); $props['Projects'] = $proj_list; } return $props; } private function buildComments(PhabricatorRepositoryCommit $commit) { $user = $this->getRequest()->getUser(); $comments = id(new PhabricatorAuditComment())->loadAllWhere( 'targetPHID = %s ORDER BY dateCreated ASC', $commit->getPHID()); $inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere( 'commitPHID = %s AND auditCommentID IS NOT NULL', $commit->getPHID()); $path_ids = mpull($inlines, 'getPathID'); $path_map = array(); if ($path_ids) { $path_map = id(new DiffusionPathQuery()) ->withPathIDs($path_ids) ->execute(); $path_map = ipull($path_map, 'path', 'id'); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); foreach ($comments as $comment) { $engine->addObject( $comment, PhabricatorAuditComment::MARKUP_FIELD_BODY); } foreach ($inlines as $inline) { $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $view = new DiffusionCommentListView(); $view->setMarkupEngine($engine); $view->setUser($user); $view->setComments($comments); $view->setInlineComments($inlines); $view->setPathMap($path_map); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $request = $this->getRequest(); $user = $request->getUser(); if (!$user->isLoggedIn()) { return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-'.$commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $form = id(new AphrontFormView()) ->setUser($user) ->setAction('/audit/addcomment/') ->addHiddenInput('commit', $commit->getPHID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setID('audit-action') ->setOptions($actions)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add Auditors')) ->setName('auditors') ->setControlID('add-auditors') ->setControlStyle('display: none') ->setID('add-auditors-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add CCs')) ->setName('ccs') ->setControlID('add-ccs') ->setControlStyle('display: none') ->setID('add-ccs-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Comments')) ->setName('content') ->setValue($draft) ->setID('audit-content') ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($is_serious ? pht('Submit') : pht('Cook the Books'))); $header = new PHUIHeaderView(); $header->setHeader( $is_serious ? pht('Audit Commit') : pht('Creative Accounting')); require_celerity_resource('phabricator-transaction-view-css'); Javelin::initBehavior( 'differential-add-reviewers-and-ccs', array( 'dynamic' => array( 'add-auditors-tokenizer' => array( 'actions' => array('add_auditors' => 1), 'src' => '/typeahead/common/users/', 'row' => 'add-auditors', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a user name...'), ), 'add-ccs-tokenizer' => array( 'actions' => array('add_ccs' => 1), 'src' => '/typeahead/common/mailable/', 'row' => 'add-ccs', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => pht('Type a user or mailing list...'), ), ), 'select' => 'audit-action', )); Javelin::initBehavior('differential-feedback-preview', array( 'uri' => '/audit/preview/'.$commit->getID().'/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action', 'previewTokenizers' => array( 'auditors' => 'add-auditors-tokenizer', 'ccs' => 'add-ccs-tokenizer', ), 'inline' => 'inline-comment-preview', 'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/', )); - $preview_panel = hsprintf( - '
-
-
- Loading preview... -
-
-
-
-
'); + $loading = phutil_tag_div( + 'aphront-panel-preview-loading-text', + pht('Loading preview...')); + + $preview_panel = phutil_tag_div( + 'aphront-panel-preview aphront-panel-flush', + array( + phutil_tag('div', array('id' => 'audit-preview'), $loading), + phutil_tag('div', array('id' => 'inline-comment-preview')) + )); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName('comment') + ->setNavigationMarker(true) + ->render(); + $comment_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($form); return phutil_tag( 'div', array( 'id' => $pane_id, ), - hsprintf( - '
%s%s%s
', - id(new PhabricatorAnchorView()) - ->setAnchorName('comment') - ->setNavigationMarker(true) - ->render(), - $comment_box, - $preview_panel)); + phutil_tag_div( + 'differential-add-comment-panel', + array($anchor, $comment_box, $preview_panel))); } /** * Return a map of available audit actions for rendering into a