diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php
index ef587e5b05..e42a14df2b 100644
--- a/src/applications/almanac/controller/AlmanacPropertyEditController.php
+++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php
@@ -1,95 +1,95 @@
 <?php
 
 final class AlmanacPropertyEditController
   extends AlmanacPropertyController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
 
     $response = $this->loadPropertyObject();
     if ($response) {
       return $response;
     }
 
     $object = $this->getPropertyObject();
 
     $cancel_uri = $object->getURI();
     $property_key = $request->getStr('key');
 
-    if (!strlen($property_key)) {
+    if (!phutil_nonempty_string($property_key)) {
       return $this->buildPropertyKeyResponse($cancel_uri, null);
     } else {
       $error = null;
       try {
         AlmanacNames::validateName($property_key);
       } catch (Exception $ex) {
         $error = $ex->getMessage();
       }
 
       // NOTE: If you enter an existing name, we're just treating it as an
       // edit operation. This might be a little confusing.
 
       if ($error !== null) {
         if ($request->isFormPost()) {
           // The user is creating a new property and picked a bad name. Give
           // them an opportunity to fix it.
           return $this->buildPropertyKeyResponse($cancel_uri, $error);
         } else {
           // The user is editing an invalid property.
           return $this->newDialog()
             ->setTitle(pht('Invalid Property'))
             ->appendParagraph(
               pht(
                 'The property name "%s" is invalid. This property can not '.
                 'be edited.',
                 $property_key))
             ->appendParagraph($error)
             ->addCancelButton($cancel_uri);
         }
       }
     }
 
     return $object->newAlmanacPropertyEditEngine()
       ->addContextParameter('objectPHID')
       ->addContextParameter('key')
       ->setTargetObject($object)
       ->setPropertyKey($property_key)
       ->setController($this)
       ->buildResponse();
   }
 
   private function buildPropertyKeyResponse($cancel_uri, $error) {
     $viewer = $this->getViewer();
     $request = $this->getRequest();
     $v_key = $request->getStr('key');
 
     if ($error !== null) {
       $e_key = pht('Invalid');
     } else {
       $e_key = true;
     }
 
     $form = id(new AphrontFormView())
       ->setUser($viewer)
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('key')
           ->setLabel(pht('Name'))
           ->setValue($v_key)
           ->setError($e_key));
 
     $errors = array();
     if ($error !== null) {
       $errors[] = $error;
     }
 
     return $this->newDialog()
       ->setTitle(pht('Add Property'))
       ->addHiddenInput('objectPHID', $request->getStr('objectPHID'))
       ->setErrors($errors)
       ->appendForm($form)
       ->addSubmitButton(pht('Continue'))
       ->addCancelButton($cancel_uri);
   }
 
 }
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php
index 1dac49bcf9..9b5512db21 100644
--- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php
@@ -1,127 +1,127 @@
 <?php
 
 final class PhabricatorAuthFactorProviderViewController
   extends PhabricatorAuthFactorProviderController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $this->getViewer();
 
     $this->requireApplicationCapability(
       AuthManageProvidersCapability::CAPABILITY);
 
     $provider = id(new PhabricatorAuthFactorProviderQuery())
       ->setViewer($viewer)
       ->withIDs(array($request->getURIData('id')))
       ->executeOne();
     if (!$provider) {
       return new Aphront404Response();
     }
 
     $crumbs = $this->buildApplicationCrumbs()
       ->addTextCrumb($provider->getObjectName())
       ->setBorder(true);
 
     $header = $this->buildHeaderView($provider);
     $properties = $this->buildPropertiesView($provider);
     $curtain = $this->buildCurtain($provider);
 
 
     $timeline = $this->buildTransactionTimeline(
       $provider,
       new PhabricatorAuthFactorProviderTransactionQuery());
     $timeline->setShouldTerminate(true);
 
     $view = id(new PHUITwoColumnView())
       ->setHeader($header)
       ->setCurtain($curtain)
       ->setMainColumn(
         array(
           $timeline,
         ))
       ->addPropertySection(pht('Details'), $properties);
 
     return $this->newPage()
       ->setTitle($provider->getDisplayName())
       ->setCrumbs($crumbs)
       ->setPageObjectPHIDs(
         array(
           $provider->getPHID(),
         ))
       ->appendChild($view);
   }
 
   private function buildHeaderView(PhabricatorAuthFactorProvider $provider) {
     $viewer = $this->getViewer();
 
     $view = id(new PHUIHeaderView())
       ->setViewer($viewer)
       ->setHeader($provider->getDisplayName())
       ->setPolicyObject($provider);
 
     $status = $provider->newStatus();
 
     $header_icon = $status->getStatusHeaderIcon();
     $header_color = $status->getStatusHeaderColor();
     $header_name = $status->getName();
     if ($header_icon !== null) {
       $view->setStatus($header_icon, $header_color, $header_name);
     }
 
     return $view;
   }
 
   private function buildPropertiesView(
     PhabricatorAuthFactorProvider $provider) {
     $viewer = $this->getViewer();
 
     $view = id(new PHUIPropertyListView())
       ->setViewer($viewer);
 
     $view->addProperty(
       pht('Factor Type'),
       $provider->getFactor()->getFactorName());
 
 
     $custom_enroll = $provider->getEnrollMessage();
-    if (strlen($custom_enroll)) {
+    if ($custom_enroll !== null && strlen($custom_enroll)) {
       $view->addSectionHeader(
         pht('Custom Enroll Message'),
         PHUIPropertyListView::ICON_SUMMARY);
       $view->addTextContent(
         new PHUIRemarkupView($viewer, $custom_enroll));
     }
 
     return $view;
   }
 
   private function buildCurtain(PhabricatorAuthFactorProvider $provider) {
     $viewer = $this->getViewer();
     $id = $provider->getID();
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $provider,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $curtain = $this->newCurtainView($provider);
 
     $curtain->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit MFA Provider'))
         ->setIcon('fa-pencil')
         ->setHref($this->getApplicationURI("mfa/edit/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(!$can_edit));
 
     $curtain->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Customize Enroll Message'))
         ->setIcon('fa-commenting-o')
         ->setHref($this->getApplicationURI("mfa/message/{$id}/"))
         ->setDisabled(!$can_edit)
         ->setWorkflow(true));
 
     return $curtain;
   }
 
 }
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index fefd9b5fd1..1ee861e44c 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,626 +1,626 @@
 <?php
 
 abstract class PhabricatorAuthFactor extends Phobject {
 
   abstract public function getFactorName();
   abstract public function getFactorShortName();
   abstract public function getFactorKey();
   abstract public function getFactorCreateHelp();
   abstract public function getFactorDescription();
   abstract public function processAddFactorForm(
     PhabricatorAuthFactorProvider $provider,
     AphrontFormView $form,
     AphrontRequest $request,
     PhabricatorUser $user);
 
   abstract public function renderValidateFactorForm(
     PhabricatorAuthFactorConfig $config,
     AphrontFormView $form,
     PhabricatorUser $viewer,
     PhabricatorAuthFactorResult $validation_result);
 
   public function getParameterName(
     PhabricatorAuthFactorConfig $config,
     $name) {
     return 'authfactor.'.$config->getID().'.'.$name;
   }
 
   public static function getAllFactors() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getFactorKey')
       ->execute();
   }
 
   protected function newConfigForUser(PhabricatorUser $user) {
     return id(new PhabricatorAuthFactorConfig())
       ->setUserPHID($user->getPHID())
       ->setFactorSecret('');
   }
 
   protected function newResult() {
     return new PhabricatorAuthFactorResult();
   }
 
   public function newIconView() {
     return id(new PHUIIconView())
       ->setIcon('fa-mobile');
   }
 
   public function canCreateNewProvider() {
     return true;
   }
 
   public function getProviderCreateDescription() {
     return null;
   }
 
   public function canCreateNewConfiguration(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user) {
     return true;
   }
 
   public function getConfigurationCreateDescription(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user) {
     return null;
   }
 
   public function getConfigurationListDetails(
     PhabricatorAuthFactorConfig $config,
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $viewer) {
     return null;
   }
 
   public function newEditEngineFields(
     PhabricatorEditEngine $engine,
     PhabricatorAuthFactorProvider $provider) {
     return array();
   }
 
   public function newChallengeStatusView(
     PhabricatorAuthFactorConfig $config,
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $viewer,
     PhabricatorAuthChallenge $challenge) {
     return null;
   }
 
   /**
    * Is this a factor which depends on the user's contact number?
    *
    * If a user has a "contact number" factor configured, they can not modify
    * or switch their primary contact number.
    *
    * @return bool True if this factor should lock contact numbers.
    */
   public function isContactNumberFactor() {
     return false;
   }
 
   abstract public function getEnrollDescription(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user);
 
   public function getEnrollButtonText(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user) {
     return pht('Continue');
   }
 
   public function getFactorOrder() {
     return 1000;
   }
 
   final public function newSortVector() {
     return id(new PhutilSortVector())
       ->addInt($this->canCreateNewProvider() ? 0 : 1)
       ->addInt($this->getFactorOrder())
       ->addString($this->getFactorName());
   }
 
   protected function newChallenge(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer) {
 
     $engine = $config->getSessionEngine();
 
     return PhabricatorAuthChallenge::initializeNewChallenge()
       ->setUserPHID($viewer->getPHID())
       ->setSessionPHID($viewer->getSession()->getPHID())
       ->setFactorPHID($config->getPHID())
       ->setIsNewChallenge(true)
       ->setWorkflowKey($engine->getWorkflowKey());
   }
 
   abstract public function getRequestHasChallengeResponse(
     PhabricatorAuthFactorConfig $config,
     AphrontRequest $response);
 
   final public function getNewIssuedChallenges(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     array $challenges) {
     assert_instances_of($challenges, 'PhabricatorAuthChallenge');
 
     $now = PhabricatorTime::getNow();
 
     // Factor implementations may need to perform writes in order to issue
     // challenges, particularly push factors like SMS.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $new_challenges = $this->newIssuedChallenges(
       $config,
       $viewer,
       $challenges);
 
     if ($this->isAuthResult($new_challenges)) {
       unset($unguarded);
       return $new_challenges;
     }
 
     assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
 
     foreach ($new_challenges as $new_challenge) {
       $ttl = $new_challenge->getChallengeTTL();
       if (!$ttl) {
         throw new Exception(
           pht('Newly issued MFA challenges must have a valid TTL!'));
       }
 
       if ($ttl < $now) {
         throw new Exception(
           pht(
             'Newly issued MFA challenges must have a future TTL. This '.
             'factor issued a bad TTL ("%s"). (Did you use a relative '.
             'time instead of an epoch?)',
             $ttl));
       }
     }
 
     foreach ($new_challenges as $challenge) {
       $challenge->save();
     }
 
     unset($unguarded);
 
     return $new_challenges;
   }
 
   abstract protected function newIssuedChallenges(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     array $challenges);
 
   final public function getResultFromIssuedChallenges(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     array $challenges) {
     assert_instances_of($challenges, 'PhabricatorAuthChallenge');
 
     $result = $this->newResultFromIssuedChallenges(
       $config,
       $viewer,
       $challenges);
 
     if ($result === null) {
       return $result;
     }
 
     if (!$this->isAuthResult($result)) {
       throw new Exception(
         pht(
           'Expected "newResultFromIssuedChallenges()" to return null or '.
           'an object of class "%s"; got something else (in "%s").',
           'PhabricatorAuthFactorResult',
           get_class($this)));
     }
 
     return $result;
   }
 
   final public function getResultForPrompt(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     AphrontRequest $request,
     array $challenges) {
     assert_instances_of($challenges, 'PhabricatorAuthChallenge');
 
     $result = $this->newResultForPrompt(
       $config,
       $viewer,
       $request,
       $challenges);
 
     if (!$this->isAuthResult($result)) {
       throw new Exception(
         pht(
           'Expected "newResultForPrompt()" to return an object of class "%s", '.
           'but it returned something else ("%s"; in "%s").',
           'PhabricatorAuthFactorResult',
           phutil_describe_type($result),
           get_class($this)));
     }
 
     return $result;
   }
 
   protected function newResultForPrompt(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     AphrontRequest $request,
     array $challenges) {
     return $this->newResult();
   }
 
   abstract protected function newResultFromIssuedChallenges(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     array $challenges);
 
   final public function getResultFromChallengeResponse(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     AphrontRequest $request,
     array $challenges) {
     assert_instances_of($challenges, 'PhabricatorAuthChallenge');
 
     $result = $this->newResultFromChallengeResponse(
       $config,
       $viewer,
       $request,
       $challenges);
 
     if (!$this->isAuthResult($result)) {
       throw new Exception(
         pht(
           'Expected "newResultFromChallengeResponse()" to return an object '.
           'of class "%s"; got something else (in "%s").',
           'PhabricatorAuthFactorResult',
           get_class($this)));
     }
 
     return $result;
   }
 
   abstract protected function newResultFromChallengeResponse(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     AphrontRequest $request,
     array $challenges);
 
   final protected function newAutomaticControl(
     PhabricatorAuthFactorResult $result) {
 
     $is_error = $result->getIsError();
     if ($is_error) {
       return $this->newErrorControl($result);
     }
 
     $is_continue = $result->getIsContinue();
     if ($is_continue) {
       return $this->newContinueControl($result);
     }
 
     $is_answered = (bool)$result->getAnsweredChallenge();
     if ($is_answered) {
       return $this->newAnsweredControl($result);
     }
 
     $is_wait = $result->getIsWait();
     if ($is_wait) {
       return $this->newWaitControl($result);
     }
 
     return null;
   }
 
   private function newWaitControl(
     PhabricatorAuthFactorResult $result) {
 
     $error = $result->getErrorMessage();
 
     $icon = $result->getIcon();
     if (!$icon) {
       $icon = id(new PHUIIconView())
         ->setIcon('fa-clock-o', 'red');
     }
 
     return id(new PHUIFormTimerControl())
       ->setIcon($icon)
       ->appendChild($error)
       ->setError(pht('Wait'));
   }
 
   private function newAnsweredControl(
     PhabricatorAuthFactorResult $result) {
 
     $icon = $result->getIcon();
     if (!$icon) {
       $icon = id(new PHUIIconView())
         ->setIcon('fa-check-circle-o', 'green');
     }
 
     return id(new PHUIFormTimerControl())
       ->setIcon($icon)
       ->appendChild(
         pht('You responded to this challenge correctly.'));
   }
 
   private function newErrorControl(
     PhabricatorAuthFactorResult $result) {
 
     $error = $result->getErrorMessage();
 
     $icon = $result->getIcon();
     if (!$icon) {
       $icon = id(new PHUIIconView())
         ->setIcon('fa-times', 'red');
     }
 
     return id(new PHUIFormTimerControl())
       ->setIcon($icon)
       ->appendChild($error)
       ->setError(pht('Error'));
   }
 
   private function newContinueControl(
     PhabricatorAuthFactorResult $result) {
 
     $error = $result->getErrorMessage();
 
     $icon = $result->getIcon();
     if (!$icon) {
       $icon = id(new PHUIIconView())
         ->setIcon('fa-commenting', 'green');
     }
 
     $control = id(new PHUIFormTimerControl())
       ->setIcon($icon)
       ->appendChild($error);
 
     $status_challenge = $result->getStatusChallenge();
     if ($status_challenge) {
       $id = $status_challenge->getID();
       $uri = "/auth/mfa/challenge/status/{$id}/";
       $control->setUpdateURI($uri);
     }
 
     return $control;
   }
 
 
 
 /* -(  Synchronizing New Factors  )------------------------------------------ */
 
 
   final protected function loadMFASyncToken(
     PhabricatorAuthFactorProvider $provider,
     AphrontRequest $request,
     AphrontFormView $form,
     PhabricatorUser $user) {
 
     // If the form included a synchronization key, load the corresponding
     // token. The user must synchronize to a key we generated because this
     // raises the barrier to theoretical attacks where an attacker might
     // provide a known key for factors like TOTP.
 
     // (We store and verify the hash of the key, not the key itself, to limit
     // how useful the data in the table is to an attacker.)
 
     $sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
     $sync_token = null;
 
     $sync_key = $request->getStr($this->getMFASyncTokenFormKey());
-    if (strlen($sync_key)) {
+    if (phutil_nonempty_string($sync_key)) {
       $sync_key_digest = PhabricatorHash::digestWithNamedKey(
         $sync_key,
         PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
 
       $sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
         ->setViewer($user)
         ->withTokenResources(array($user->getPHID()))
         ->withTokenTypes(array($sync_type))
         ->withExpired(false)
         ->withTokenCodes(array($sync_key_digest))
         ->executeOne();
     }
 
     if (!$sync_token) {
 
       // Don't generate a new sync token if there are too many outstanding
       // tokens already. This is mostly relevant for push factors like SMS,
       // where generating a token has the side effect of sending a user a
       // message.
 
       $outstanding_limit = 10;
       $outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
         ->setViewer($user)
         ->withTokenResources(array($user->getPHID()))
         ->withTokenTypes(array($sync_type))
         ->withExpired(false)
         ->execute();
       if (count($outstanding_tokens) > $outstanding_limit) {
         throw new Exception(
           pht(
             'Your account has too many outstanding, incomplete MFA '.
             'synchronization attempts. Wait an hour and try again.'));
       }
 
       $now = PhabricatorTime::getNow();
 
       $sync_key = Filesystem::readRandomCharacters(32);
       $sync_key_digest = PhabricatorHash::digestWithNamedKey(
         $sync_key,
         PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
       $sync_ttl = $this->getMFASyncTokenTTL();
 
       $sync_token = id(new PhabricatorAuthTemporaryToken())
         ->setIsNewTemporaryToken(true)
         ->setTokenResource($user->getPHID())
         ->setTokenType($sync_type)
         ->setTokenCode($sync_key_digest)
         ->setTokenExpires($now + $sync_ttl);
 
       $properties = $this->newMFASyncTokenProperties(
         $provider,
         $user);
 
       if ($this->isAuthResult($properties)) {
         return $properties;
       }
 
       foreach ($properties as $key => $value) {
         $sync_token->setTemporaryTokenProperty($key, $value);
       }
 
       $sync_token->save();
     }
 
     $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
 
     return $sync_token;
   }
 
   protected function newMFASyncTokenProperties(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user) {
     return array();
   }
 
   private function getMFASyncTokenFormKey() {
     return 'sync.key';
   }
 
   private function getMFASyncTokenTTL() {
     return phutil_units('1 hour in seconds');
   }
 
   final protected function getChallengeForCurrentContext(
     PhabricatorAuthFactorConfig $config,
     PhabricatorUser $viewer,
     array $challenges) {
 
     $session_phid = $viewer->getSession()->getPHID();
     $engine = $config->getSessionEngine();
     $workflow_key = $engine->getWorkflowKey();
 
     foreach ($challenges as $challenge) {
       if ($challenge->getSessionPHID() !== $session_phid) {
         continue;
       }
 
       if ($challenge->getWorkflowKey() !== $workflow_key) {
         continue;
       }
 
       if ($challenge->getIsCompleted()) {
         continue;
       }
 
       if ($challenge->getIsReusedChallenge()) {
         continue;
       }
 
       return $challenge;
     }
 
     return null;
   }
 
 
   /**
    * @phutil-external-symbol class QRcode
    */
   final protected function newQRCode($uri) {
     $root = dirname(phutil_get_library_root('phabricator'));
     require_once $root.'/externals/phpqrcode/phpqrcode.php';
 
     $lines = QRcode::text($uri);
 
     $total_width = 240;
     $cell_size = floor($total_width / count($lines));
 
     $rows = array();
     foreach ($lines as $line) {
       $cells = array();
       for ($ii = 0; $ii < strlen($line); $ii++) {
         if ($line[$ii] == '1') {
           $color = '#000';
         } else {
           $color = '#fff';
         }
 
         $cells[] = phutil_tag(
           'td',
           array(
             'width' => $cell_size,
             'height' => $cell_size,
             'style' => 'background: '.$color,
           ),
           '');
       }
       $rows[] = phutil_tag('tr', array(), $cells);
     }
 
     return phutil_tag(
       'table',
       array(
         'style' => 'margin: 24px auto;',
       ),
       $rows);
   }
 
   final protected function getInstallDisplayName() {
     $uri = PhabricatorEnv::getURI('/');
     $uri = new PhutilURI($uri);
     return $uri->getDomain();
   }
 
   final protected function getChallengeResponseParameterName(
     PhabricatorAuthFactorConfig $config) {
     return $this->getParameterName($config, 'mfa.response');
   }
 
   final protected function getChallengeResponseFromRequest(
     PhabricatorAuthFactorConfig $config,
     AphrontRequest $request) {
 
     $name = $this->getChallengeResponseParameterName($config);
 
     $value = $request->getStr($name);
     $value = (string)$value;
     $value = trim($value);
 
     return $value;
   }
 
   final protected function hasCSRF(PhabricatorAuthFactorConfig $config) {
     $engine = $config->getSessionEngine();
     $request = $engine->getRequest();
 
     if (!$request->isHTTPPost()) {
       return false;
     }
 
     return $request->validateCSRF();
   }
 
   final protected function loadConfigurationsForProvider(
     PhabricatorAuthFactorProvider $provider,
     PhabricatorUser $user) {
 
     return id(new PhabricatorAuthFactorConfigQuery())
       ->setViewer($user)
       ->withUserPHIDs(array($user->getPHID()))
       ->withFactorProviderPHIDs(array($provider->getPHID()))
       ->execute();
   }
 
   final protected function isAuthResult($object) {
     return ($object instanceof PhabricatorAuthFactorResult);
   }
 
 }
diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php
index 0b740e5fa7..acc05572fc 100644
--- a/src/applications/auth/storage/PhabricatorAuthChallenge.php
+++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php
@@ -1,272 +1,275 @@
 <?php
 
 final class PhabricatorAuthChallenge
   extends PhabricatorAuthDAO
   implements PhabricatorPolicyInterface {
 
   protected $userPHID;
   protected $factorPHID;
   protected $sessionPHID;
   protected $workflowKey;
   protected $challengeKey;
   protected $challengeTTL;
   protected $responseDigest;
   protected $responseTTL;
   protected $isCompleted;
   protected $properties = array();
 
   private $responseToken;
   private $isNewChallenge;
 
   const HTTPKEY = '__hisec.challenges__';
   const TOKEN_DIGEST_KEY = 'auth.challenge.token';
 
   public static function initializeNewChallenge() {
     return id(new self())
       ->setIsCompleted(0);
   }
 
   public static function newHTTPParametersFromChallenges(array $challenges) {
     assert_instances_of($challenges, __CLASS__);
 
     $token_list = array();
     foreach ($challenges as $challenge) {
       $token = $challenge->getResponseToken();
       if ($token) {
         $token_list[] = sprintf(
           '%s:%s',
           $challenge->getPHID(),
           $token->openEnvelope());
       }
     }
 
     if (!$token_list) {
       return array();
     }
 
     $token_list = implode(' ', $token_list);
 
     return array(
       self::HTTPKEY => $token_list,
     );
   }
 
   public static function newChallengeResponsesFromRequest(
     array $challenges,
     AphrontRequest $request) {
     assert_instances_of($challenges, __CLASS__);
 
     $token_list = $request->getStr(self::HTTPKEY);
+    if ($token_list === null) {
+      return;
+    }
     $token_list = explode(' ', $token_list);
 
     $token_map = array();
     foreach ($token_list as $token_element) {
       $token_element = trim($token_element, ' ');
 
       if (!strlen($token_element)) {
         continue;
       }
 
       // NOTE: This error message is intentionally not printing the token to
       // avoid disclosing it. As a result, it isn't terribly useful, but no
       // normal user should ever end up here.
       if (!preg_match('/^[^:]+:/', $token_element)) {
         throw new Exception(
           pht(
             'This request included an improperly formatted MFA challenge '.
             'token and can not be processed.'));
       }
 
       list($phid, $token) = explode(':', $token_element, 2);
 
       if (isset($token_map[$phid])) {
         throw new Exception(
           pht(
             'This request improperly specifies an MFA challenge token ("%s") '.
             'multiple times and can not be processed.',
             $phid));
       }
 
       $token_map[$phid] = new PhutilOpaqueEnvelope($token);
     }
 
     $challenges = mpull($challenges, null, 'getPHID');
 
     $now = PhabricatorTime::getNow();
     foreach ($challenges as $challenge_phid => $challenge) {
       // If the response window has expired, don't attach the token.
       if ($challenge->getResponseTTL() < $now) {
         continue;
       }
 
       $token = idx($token_map, $challenge_phid);
       if (!$token) {
         continue;
       }
 
       $challenge->setResponseToken($token);
     }
   }
 
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'properties' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_COLUMN_SCHEMA => array(
         'challengeKey' => 'text255',
         'challengeTTL' => 'epoch',
         'workflowKey' => 'text255',
         'responseDigest' => 'text255?',
         'responseTTL' => 'epoch?',
         'isCompleted' => 'bool',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_issued' => array(
           'columns' => array('userPHID', 'challengeTTL'),
         ),
         'key_collection' => array(
           'columns' => array('challengeTTL'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function getPHIDType() {
     return PhabricatorAuthChallengePHIDType::TYPECONST;
   }
 
   public function getIsReusedChallenge() {
     if ($this->getIsCompleted()) {
       return true;
     }
 
     if (!$this->getIsAnsweredChallenge()) {
       return false;
     }
 
     // If the challenge has been answered but the client has provided a token
     // proving that they answered it, this is still a valid response.
     if ($this->getResponseToken()) {
       return false;
     }
 
     return true;
   }
 
   public function getIsAnsweredChallenge() {
     return (bool)$this->getResponseDigest();
   }
 
   public function markChallengeAsAnswered($ttl) {
     $token = Filesystem::readRandomCharacters(32);
     $token = new PhutilOpaqueEnvelope($token);
 
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $this
       ->setResponseToken($token)
       ->setResponseTTL($ttl)
       ->save();
 
     unset($unguarded);
 
     return $this;
   }
 
   public function markChallengeAsCompleted() {
     return $this
       ->setIsCompleted(true)
       ->save();
   }
 
   public function setResponseToken(PhutilOpaqueEnvelope $token) {
     if (!$this->getUserPHID()) {
       throw new PhutilInvalidStateException('setUserPHID');
     }
 
     if ($this->responseToken) {
       throw new Exception(
         pht(
           'This challenge already has a response token; you can not '.
           'set a new response token.'));
     }
 
     if (preg_match('/ /', $token->openEnvelope())) {
       throw new Exception(
         pht(
           'The response token for this challenge is invalid: response '.
           'tokens may not include spaces.'));
     }
 
     $digest = PhabricatorHash::digestWithNamedKey(
       $token->openEnvelope(),
       self::TOKEN_DIGEST_KEY);
 
     if ($this->responseDigest !== null) {
       if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {
         throw new Exception(
           pht(
             'Invalid response token for this challenge: token digest does '.
             'not match stored digest.'));
       }
     } else {
       $this->responseDigest = $digest;
     }
 
     $this->responseToken = $token;
 
     return $this;
   }
 
   public function getResponseToken() {
     return $this->responseToken;
   }
 
   public function setResponseDigest($value) {
     throw new Exception(
       pht(
         'You can not set the response digest for a challenge directly. '.
         'Instead, set a response token. A response digest will be computed '.
         'automatically.'));
   }
 
   public function setProperty($key, $value) {
     $this->properties[$key] = $value;
     return $this;
   }
 
   public function getProperty($key, $default = null) {
     return $this->properties[$key];
   }
 
   public function setIsNewChallenge($is_new_challenge) {
     $this->isNewChallenge = $is_new_challenge;
     return $this;
   }
 
   public function getIsNewChallenge() {
     return $this->isNewChallenge;
   }
 
 
 /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
     );
   }
 
   public function getPolicy($capability) {
     return PhabricatorPolicies::POLICY_NOONE;
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
     return ($viewer->getPHID() === $this->getUserPHID());
   }
 
 }
diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php
index 99fd18878e..8358780991 100644
--- a/src/applications/conpherence/query/ConpherenceThreadQuery.php
+++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php
@@ -1,344 +1,345 @@
 <?php
 
 final class ConpherenceThreadQuery
   extends PhabricatorCursorPagedPolicyAwareQuery {
 
   const TRANSACTION_LIMIT = 100;
 
   private $phids;
   private $ids;
   private $participantPHIDs;
   private $needParticipants;
   private $needTransactions;
   private $afterTransactionID;
   private $beforeTransactionID;
   private $transactionLimit;
   private $fulltext;
   private $needProfileImage;
 
   public function needParticipants($need) {
     $this->needParticipants = $need;
     return $this;
   }
 
   public function needProfileImage($need) {
     $this->needProfileImage = $need;
     return $this;
   }
 
   public function needTransactions($need_transactions) {
     $this->needTransactions = $need_transactions;
     return $this;
   }
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withParticipantPHIDs(array $phids) {
     $this->participantPHIDs = $phids;
     return $this;
   }
 
   public function setAfterTransactionID($id) {
     $this->afterTransactionID = $id;
     return $this;
   }
 
   public function setBeforeTransactionID($id) {
     $this->beforeTransactionID = $id;
     return $this;
   }
 
   public function setTransactionLimit($transaction_limit) {
     $this->transactionLimit = $transaction_limit;
     return $this;
   }
 
   public function getTransactionLimit() {
     return $this->transactionLimit;
   }
 
   public function withFulltext($query) {
     $this->fulltext = $query;
     return $this;
   }
 
   public function withTitleNgrams($ngrams) {
     return $this->withNgramsConstraint(
       id(new ConpherenceThreadTitleNgrams()),
       $ngrams);
   }
 
   protected function loadPage() {
     $table = new ConpherenceThread();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT thread.* FROM %T thread %Q %Q %Q %Q %Q',
       $table->getTableName(),
       $this->buildJoinClause($conn_r),
       $this->buildWhereClause($conn_r),
       $this->buildGroupClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     $conpherences = $table->loadAllFromArray($data);
 
     if ($conpherences) {
       $conpherences = mpull($conpherences, null, 'getPHID');
       $this->loadParticipantsAndInitHandles($conpherences);
       if ($this->needParticipants) {
         $this->loadCoreHandles($conpherences, 'getParticipantPHIDs');
       }
       if ($this->needTransactions) {
         $this->loadTransactionsAndHandles($conpherences);
       }
       if ($this->needProfileImage) {
         $default = null;
         $file_phids = mpull($conpherences, 'getProfileImagePHID');
         $file_phids = array_filter($file_phids);
         if ($file_phids) {
           $files = id(new PhabricatorFileQuery())
             ->setParentQuery($this)
             ->setViewer($this->getViewer())
             ->withPHIDs($file_phids)
             ->execute();
           $files = mpull($files, null, 'getPHID');
         } else {
           $files = array();
         }
 
         foreach ($conpherences as $conpherence) {
           $file = idx($files, $conpherence->getProfileImagePHID());
           if (!$file) {
             if (!$default) {
               $default = PhabricatorFile::loadBuiltin(
                 $this->getViewer(),
                 'conpherence.png');
             }
             $file = $default;
           }
           $conpherence->attachProfileImageFile($file);
         }
       }
     }
 
     return $conpherences;
   }
 
   protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
-    if ($this->participantPHIDs !== null || strlen($this->fulltext)) {
+    if ($this->participantPHIDs !== null
+      || ($this->fulltext !== null && strlen($this->fulltext))) {
       return qsprintf($conn_r, 'GROUP BY thread.id');
     } else {
       return $this->buildApplicationSearchGroupClause($conn_r);
     }
   }
 
   protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
     $joins = parent::buildJoinClauseParts($conn);
 
     if ($this->participantPHIDs !== null) {
       $joins[] = qsprintf(
         $conn,
         'JOIN %T p ON p.conpherencePHID = thread.phid',
         id(new ConpherenceParticipant())->getTableName());
     }
 
-    if (strlen($this->fulltext)) {
+    if ($this->fulltext !== null && strlen($this->fulltext)) {
       $joins[] = qsprintf(
         $conn,
         'JOIN %T idx ON idx.threadPHID = thread.phid',
         id(new ConpherenceIndex())->getTableName());
     }
 
     // See note in buildWhereClauseParts() about this optimization.
     $viewer = $this->getViewer();
     if (!$viewer->isOmnipotent() && $viewer->isLoggedIn()) {
       $joins[] = qsprintf(
         $conn,
         'LEFT JOIN %T vp ON vp.conpherencePHID = thread.phid
           AND vp.participantPHID = %s',
         id(new ConpherenceParticipant())->getTableName(),
         $viewer->getPHID());
     }
 
     return $joins;
   }
 
   protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
     $where = parent::buildWhereClauseParts($conn);
 
     // Optimize policy filtering of private rooms. If we are not looking for
     // particular rooms by ID or PHID, we can just skip over any rooms with
     // "View Policy: Room Participants" if the viewer isn't a participant: we
     // know they won't be able to see the room.
     // This avoids overheating browse/search queries, since it's common for
     // a large number of rooms to be private and have this view policy.
     $viewer = $this->getViewer();
 
     $can_optimize =
       !$viewer->isOmnipotent() &&
       ($this->ids === null) &&
       ($this->phids === null);
 
     if ($can_optimize) {
       $members_policy = id(new ConpherenceThreadMembersPolicyRule())
         ->getObjectPolicyFullKey();
       $policies = array(
         $members_policy,
         PhabricatorPolicies::POLICY_USER,
         PhabricatorPolicies::POLICY_ADMIN,
         PhabricatorPolicies::POLICY_NOONE,
       );
 
       if ($viewer->isLoggedIn()) {
         $where[] = qsprintf(
           $conn,
           'thread.viewPolicy NOT IN (%Ls) OR vp.participantPHID = %s',
           $policies,
           $viewer->getPHID());
       } else {
         $where[] = qsprintf(
           $conn,
           'thread.viewPolicy NOT IN (%Ls)',
           $policies);
       }
     }
 
     if ($this->ids !== null) {
       $where[] = qsprintf(
         $conn,
         'thread.id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids !== null) {
       $where[] = qsprintf(
         $conn,
         'thread.phid IN (%Ls)',
         $this->phids);
     }
 
     if ($this->participantPHIDs !== null) {
       $where[] = qsprintf(
         $conn,
         'p.participantPHID IN (%Ls)',
         $this->participantPHIDs);
     }
 
-    if (strlen($this->fulltext)) {
+    if ($this->fulltext !== null && strlen($this->fulltext)) {
       $where[] = qsprintf(
         $conn,
         'MATCH(idx.corpus) AGAINST (%s IN BOOLEAN MODE)',
         $this->fulltext);
     }
 
     return $where;
   }
 
   private function loadParticipantsAndInitHandles(array $conpherences) {
     $participants = id(new ConpherenceParticipant())
       ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences));
     $map = mgroup($participants, 'getConpherencePHID');
 
     foreach ($conpherences as $current_conpherence) {
       $conpherence_phid = $current_conpherence->getPHID();
 
       $conpherence_participants = idx(
         $map,
         $conpherence_phid,
         array());
 
       $conpherence_participants = mpull(
         $conpherence_participants,
         null,
         'getParticipantPHID');
 
       $current_conpherence->attachParticipants($conpherence_participants);
       $current_conpherence->attachHandles(array());
     }
 
     return $this;
   }
 
   private function loadCoreHandles(
     array $conpherences,
     $method) {
 
     $handle_phids = array();
     foreach ($conpherences as $conpherence) {
       $handle_phids[$conpherence->getPHID()] =
         $conpherence->$method();
     }
     $flat_phids = array_mergev($handle_phids);
     $viewer = $this->getViewer();
     $handles = $viewer->loadHandles($flat_phids);
     $handles = iterator_to_array($handles);
     foreach ($handle_phids as $conpherence_phid => $phids) {
       $conpherence = $conpherences[$conpherence_phid];
       $conpherence->attachHandles(
         $conpherence->getHandles() + array_select_keys($handles, $phids));
     }
     return $this;
   }
 
   private function loadTransactionsAndHandles(array $conpherences) {
     // NOTE: This is older code which has been modernized to the minimum
     // standard required by T13266. It probably isn't the best available
     // approach to the problems it solves.
 
     $limit = $this->getTransactionLimit();
     if ($limit) {
       // fetch an extra for "show older" scenarios
       $limit = $limit + 1;
     } else {
       $limit = 0xFFFF;
     }
 
     $pager = id(new AphrontCursorPagerView())
       ->setPageSize($limit);
 
     // We have to flip these for the underlying query class. The semantics of
     // paging are tricky business.
     if ($this->afterTransactionID) {
       $pager->setBeforeID($this->afterTransactionID);
     } else if ($this->beforeTransactionID) {
       $pager->setAfterID($this->beforeTransactionID);
     }
 
     $transactions = id(new ConpherenceTransactionQuery())
       ->setViewer($this->getViewer())
       ->withObjectPHIDs(array_keys($conpherences))
       ->needHandles(true)
       ->executeWithCursorPager($pager);
 
     $transactions = mgroup($transactions, 'getObjectPHID');
     foreach ($conpherences as $phid => $conpherence) {
       $current_transactions = idx($transactions, $phid, array());
       $handles = array();
       foreach ($current_transactions as $transaction) {
         $handles += $transaction->getHandles();
       }
       $conpherence->attachHandles($conpherence->getHandles() + $handles);
       $conpherence->attachTransactions($current_transactions);
     }
     return $this;
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorConpherenceApplication';
   }
 
   protected function getPrimaryTableAlias() {
     return 'thread';
   }
 
 }
diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
index cbaf43b0a9..1bb0d71f7a 100644
--- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
+++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php
@@ -1,445 +1,445 @@
 <?php
 
 final class ConpherenceThreadSearchEngine
   extends PhabricatorApplicationSearchEngine {
 
   public function getResultTypeDescription() {
     return pht('Conpherence Rooms');
   }
 
   public function getApplicationClassName() {
     return 'PhabricatorConpherenceApplication';
   }
 
   public function newQuery() {
     return id(new ConpherenceThreadQuery())
       ->needProfileImage(true);
   }
 
   protected function buildCustomSearchFields() {
     return array(
       id(new PhabricatorUsersSearchField())
         ->setLabel(pht('Participants'))
         ->setKey('participants')
         ->setAliases(array('participant')),
       id(new PhabricatorSearchDatasourceField())
         ->setLabel(pht('Rooms'))
         ->setKey('phids')
         ->setDescription(pht('Search by room titles.'))
         ->setDatasource(id(new ConpherenceThreadDatasource())),
       id(new PhabricatorSearchTextField())
         ->setLabel(pht('Room Contains Words'))
         ->setKey('fulltext'),
     );
   }
 
   protected function getDefaultFieldOrder() {
     return array(
       'participants',
       '...',
     );
   }
 
   protected function shouldShowOrderField() {
     return false;
   }
 
   protected function buildQueryFromParameters(array $map) {
     $query = $this->newQuery();
     if ($map['participants']) {
       $query->withParticipantPHIDs($map['participants']);
     }
     if ($map['fulltext']) {
       $query->withFulltext($map['fulltext']);
     }
     if ($map['phids']) {
       $query->withPHIDs($map['phids']);
     }
     return $query;
   }
 
   protected function getURI($path) {
     return '/conpherence/search/'.$path;
   }
 
   protected function getBuiltinQueryNames() {
     $names = array();
 
     $names['all'] = pht('All Rooms');
 
     if ($this->requireViewer()->isLoggedIn()) {
       $names['participant'] = pht('Joined Rooms');
     }
 
     return $names;
   }
 
   public function buildSavedQueryFromBuiltin($query_key) {
 
     $query = $this->newSavedQuery();
     $query->setQueryKey($query_key);
 
     switch ($query_key) {
       case 'all':
         return $query;
       case 'participant':
         return $query->setParameter(
           'participants',
           array($this->requireViewer()->getPHID()));
     }
 
     return parent::buildSavedQueryFromBuiltin($query_key);
   }
 
   protected function renderResultList(
     array $conpherences,
     PhabricatorSavedQuery $query,
     array $handles) {
     assert_instances_of($conpherences, 'ConpherenceThread');
 
     $viewer = $this->requireViewer();
 
     $policy_objects = ConpherenceThread::loadViewPolicyObjects(
       $viewer,
       $conpherences);
 
     $engines = array();
 
     $fulltext = $query->getParameter('fulltext');
-    if (strlen($fulltext) && $conpherences) {
+    if ($fulltext !== null && strlen($fulltext) && $conpherences) {
       $context = $this->loadContextMessages($conpherences, $fulltext);
 
       $author_phids = array();
       foreach ($context as $phid => $messages) {
         $conpherence = $conpherences[$phid];
 
         $engine = id(new PhabricatorMarkupEngine())
           ->setViewer($viewer)
           ->setContextObject($conpherence);
 
         foreach ($messages as $group) {
           foreach ($group as $message) {
             $xaction = $message['xaction'];
             if ($xaction) {
               $author_phids[] = $xaction->getAuthorPHID();
               $engine->addObject(
                 $xaction->getComment(),
                 PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
             }
           }
         }
         $engine->process();
 
         $engines[$phid] = $engine;
       }
 
       $handles = $viewer->loadHandles($author_phids);
       $handles = iterator_to_array($handles);
     } else {
       $context = array();
     }
 
     $content = array();
     $list = new PHUIObjectItemListView();
     $list->setUser($viewer);
     foreach ($conpherences as $conpherence_phid => $conpherence) {
       $created = phabricator_date($conpherence->getDateCreated(), $viewer);
       $title = $conpherence->getTitle();
       $monogram = $conpherence->getMonogram();
 
       $icon_name = $conpherence->getPolicyIconName($policy_objects);
       $icon = id(new PHUIIconView())
         ->setIcon($icon_name);
 
-      if (!strlen($fulltext)) {
+      if ($fulltext === null || !strlen($fulltext)) {
         $item = id(new PHUIObjectItemView())
           ->setObjectName($conpherence->getMonogram())
           ->setHeader($title)
           ->setHref('/'.$conpherence->getMonogram())
           ->setObject($conpherence)
           ->setImageURI($conpherence->getProfileImageURI())
           ->addIcon('none', $created)
           ->addIcon(
             'none',
             pht('Messages: %d', $conpherence->getMessageCount()))
           ->addAttribute(
             array(
               $icon,
               ' ',
               pht(
                 'Last updated %s',
                 phabricator_datetime($conpherence->getDateModified(), $viewer)),
             ));
           $list->addItem($item);
       } else {
         $messages = idx($context, $conpherence_phid);
         $box = array();
         $list = null;
         if ($messages) {
           foreach ($messages as $group) {
             $rows = array();
             foreach ($group as $message) {
               $xaction = $message['xaction'];
               if (!$xaction) {
                 continue;
               }
 
               $view = id(new ConpherenceTransactionView())
                 ->setUser($viewer)
                 ->setHandles($handles)
                 ->setMarkupEngine($engines[$conpherence_phid])
                 ->setConpherenceThread($conpherence)
                 ->setConpherenceTransaction($xaction)
                 ->setSearchResult(true)
                 ->addClass('conpherence-fulltext-result');
 
               if ($message['match']) {
                 $view->addClass('conpherence-fulltext-match');
               }
 
               $rows[] = $view;
             }
             $box[] = id(new PHUIBoxView())
               ->appendChild($rows)
               ->addClass('conpherence-fulltext-results');
           }
         }
         $header = id(new PHUIHeaderView())
           ->setHeader($title)
           ->setHeaderIcon($icon_name)
           ->setHref('/'.$monogram);
 
         $content[] = id(new PHUIObjectBoxView())
           ->setHeader($header)
           ->appendChild($box);
       }
     }
 
     if ($list) {
       $content = $list;
     } else {
       $content = id(new PHUIBoxView())
         ->addClass('conpherence-search-room-results')
         ->appendChild($content);
     }
 
     $result = new PhabricatorApplicationSearchResultView();
     $result->setContent($content);
     $result->setNoDataString(pht('No results found.'));
 
     return $result;
   }
 
   private function loadContextMessages(array $threads, $fulltext) {
     $phids = mpull($threads, 'getPHID');
 
     // We want to load a few messages for each thread in the result list, to
     // show some of the actual content hits to help the user find what they
     // are looking for.
 
     // This method is trying to batch this lookup in most cases, so we do
     // between one and "a handful" of queries instead of one per thread in
     // most cases. To do this:
     //
     //   - Load a big block of results for all of the threads.
     //   - If we didn't get a full block back, we have everything that matches
     //     the query. Sort it out and exit.
     //   - Otherwise, some threads had a ton of hits, so we might not be
     //     getting everything we want (we could be getting back 1,000 hits for
     //     the first thread). Remove any threads which we have enough results
     //     for and try again.
     //   - Repeat until we have everything or every thread has enough results.
     //
     // In the worst case, we could end up degrading to one query per thread,
     // but this is incredibly unlikely on real data.
 
     // Size of the result blocks we're going to load.
     $limit = 1000;
 
     // Number of messages we want for each thread.
     $want = 3;
 
     $need = $phids;
     $hits = array();
     while ($need) {
       $rows = id(new ConpherenceFulltextQuery())
         ->withThreadPHIDs($need)
         ->withFulltext($fulltext)
         ->setLimit($limit)
         ->execute();
 
       foreach ($rows as $row) {
         $hits[$row['threadPHID']][] = $row;
       }
 
       if (count($rows) < $limit) {
         break;
       }
 
       foreach ($need as $key => $phid) {
         if (count($hits[$phid]) >= $want) {
           unset($need[$key]);
         }
       }
     }
 
     // Now that we have all the fulltext matches, throw away any extras that we
     // aren't going to render so we don't need to do lookups on them.
     foreach ($hits as $phid => $rows) {
       if (count($rows) > $want) {
         $hits[$phid] = array_slice($rows, 0, $want);
       }
     }
 
     // For each fulltext match, we want to render a message before and after
     // the match to give it some context. We already know the transactions
     // before each match because the rows have a "previousTransactionPHID",
     // but we need to do one more query to figure out the transactions after
     // each match.
 
     // Collect the transactions we want to find the next transactions for.
     $after = array();
     foreach ($hits as $phid => $rows) {
       foreach ($rows as $row) {
         $after[] = $row['transactionPHID'];
       }
     }
 
     // Look up the next transactions.
     if ($after) {
       $after_rows = id(new ConpherenceFulltextQuery())
         ->withPreviousTransactionPHIDs($after)
         ->execute();
     } else {
       $after_rows = array();
     }
 
     // Build maps from PHIDs to the previous and next PHIDs.
     $prev_map = array();
     $next_map = array();
     foreach ($after_rows as $row) {
       $next_map[$row['previousTransactionPHID']] = $row['transactionPHID'];
     }
 
     foreach ($hits as $phid => $rows) {
       foreach ($rows as $row) {
         $prev = $row['previousTransactionPHID'];
         if ($prev) {
           $prev_map[$row['transactionPHID']] = $prev;
           $next_map[$prev] = $row['transactionPHID'];
         }
       }
     }
 
     // Now we're going to collect the actual transaction PHIDs, in order, that
     // we want to show for each thread.
     $groups = array();
     foreach ($hits as $thread_phid => $rows) {
       $rows = ipull($rows, null, 'transactionPHID');
       $done = array();
       foreach ($rows as $phid => $row) {
         if (isset($done[$phid])) {
           continue;
         }
         $done[$phid] = true;
 
         $group = array();
 
         // Walk backward, finding all the previous results. We can just keep
         // going until we run out of results because we've only loaded things
         // that we want to show.
         $prev = $phid;
         while (true) {
           if (!isset($prev_map[$prev])) {
             // No previous transaction, so we're done.
             break;
           }
 
           $prev = $prev_map[$prev];
 
           if (isset($rows[$prev])) {
             $match = true;
             $done[$prev] = true;
           } else {
             $match = false;
           }
 
           $group[] = array(
             'phid' => $prev,
             'match' => $match,
           );
         }
 
         if (count($group) > 1) {
           $group = array_reverse($group);
         }
 
         $group[] = array(
           'phid' => $phid,
           'match' => true,
         );
 
         $next = $phid;
         while (true) {
           if (!isset($next_map[$next])) {
             break;
           }
 
           $next = $next_map[$next];
 
           if (isset($rows[$next])) {
             $match = true;
             $done[$next] = true;
           } else {
             $match = false;
           }
 
           $group[] = array(
             'phid' => $next,
             'match' => $match,
           );
         }
 
         $groups[$thread_phid][] = $group;
       }
     }
 
     // Load all the actual transactions we need.
     $xaction_phids = array();
     foreach ($groups as $thread_phid => $group) {
       foreach ($group as $list) {
         foreach ($list as $item) {
           $xaction_phids[] = $item['phid'];
         }
       }
     }
 
     if ($xaction_phids) {
       $xactions = id(new ConpherenceTransactionQuery())
         ->setViewer($this->requireViewer())
         ->withPHIDs($xaction_phids)
         ->needComments(true)
         ->execute();
       $xactions = mpull($xactions, null, 'getPHID');
     } else {
       $xactions = array();
     }
 
     foreach ($groups as $thread_phid => $group) {
       foreach ($group as $key => $list) {
         foreach ($list as $lkey => $item) {
           $xaction = idx($xactions, $item['phid']);
           if ($xaction->shouldHide()) {
             continue;
           }
           $groups[$thread_phid][$key][$lkey]['xaction'] = $xaction;
         }
       }
     }
 
     // TODO: Sort the groups chronologically?
 
     return $groups;
   }
 
 }
diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php
index 2d6527ec96..6bd23e8502 100644
--- a/src/applications/diviner/query/DivinerBookQuery.php
+++ b/src/applications/diviner/query/DivinerBookQuery.php
@@ -1,200 +1,200 @@
 <?php
 
 final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery {
 
   private $ids;
   private $phids;
   private $names;
   private $nameLike;
   private $namePrefix;
   private $repositoryPHIDs;
 
   private $needProjectPHIDs;
   private $needRepositories;
 
   public function withIDs(array $ids) {
     $this->ids = $ids;
     return $this;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function withNameLike($name) {
     $this->nameLike = $name;
     return $this;
   }
 
   public function withNames(array $names) {
     $this->names = $names;
     return $this;
   }
 
   public function withNamePrefix($prefix) {
     $this->namePrefix = $prefix;
     return $this;
   }
 
   public function withRepositoryPHIDs(array $repository_phids) {
     $this->repositoryPHIDs = $repository_phids;
     return $this;
   }
 
   public function needProjectPHIDs($need_phids) {
     $this->needProjectPHIDs = $need_phids;
     return $this;
   }
 
   public function needRepositories($need_repositories) {
     $this->needRepositories = $need_repositories;
     return $this;
   }
 
   protected function loadPage() {
     $table = new DivinerLiveBook();
     $conn_r = $table->establishConnection('r');
 
     $data = queryfx_all(
       $conn_r,
       'SELECT * FROM %T %Q %Q %Q',
       $table->getTableName(),
       $this->buildWhereClause($conn_r),
       $this->buildOrderClause($conn_r),
       $this->buildLimitClause($conn_r));
 
     return $table->loadAllFromArray($data);
   }
 
   protected function didFilterPage(array $books) {
     assert_instances_of($books, 'DivinerLiveBook');
 
     if ($this->needRepositories) {
       $repositories = id(new PhabricatorRepositoryQuery())
         ->setViewer($this->getViewer())
         ->withPHIDs(mpull($books, 'getRepositoryPHID'))
         ->execute();
       $repositories = mpull($repositories, null, 'getPHID');
 
       foreach ($books as $key => $book) {
         if ($book->getRepositoryPHID() === null) {
           $book->attachRepository(null);
           continue;
         }
 
         $repository = idx($repositories, $book->getRepositoryPHID());
 
         if (!$repository) {
           $this->didRejectResult($book);
           unset($books[$key]);
           continue;
         }
 
         $book->attachRepository($repository);
       }
     }
 
     if ($this->needProjectPHIDs) {
       $edge_query = id(new PhabricatorEdgeQuery())
         ->withSourcePHIDs(mpull($books, 'getPHID'))
         ->withEdgeTypes(
           array(
             PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
           ));
       $edge_query->execute();
 
       foreach ($books as $book) {
         $project_phids = $edge_query->getDestinationPHIDs(
           array(
             $book->getPHID(),
           ));
         $book->attachProjectPHIDs($project_phids);
       }
     }
 
     return $books;
   }
 
   protected function buildWhereClause(AphrontDatabaseConnection $conn) {
     $where = array();
 
     if ($this->ids) {
       $where[] = qsprintf(
         $conn,
         'id IN (%Ld)',
         $this->ids);
     }
 
     if ($this->phids) {
       $where[] = qsprintf(
         $conn,
         'phid IN (%Ls)',
         $this->phids);
     }
 
-    if (strlen($this->nameLike)) {
+    if ($this->nameLike !== null && strlen($this->nameLike)) {
       $where[] = qsprintf(
         $conn,
         'name LIKE %~',
         $this->nameLike);
     }
 
     if ($this->names !== null) {
       $where[] = qsprintf(
         $conn,
         'name IN (%Ls)',
         $this->names);
     }
 
-    if (strlen($this->namePrefix)) {
+    if ($this->namePrefix !== null && strlen($this->namePrefix)) {
       $where[] = qsprintf(
         $conn,
         'name LIKE %>',
         $this->namePrefix);
     }
 
     if ($this->repositoryPHIDs !== null) {
       $where[] = qsprintf(
         $conn,
         'repositoryPHID IN (%Ls)',
         $this->repositoryPHIDs);
     }
 
     $where[] = $this->buildPagingClause($conn);
 
     return $this->formatWhereClause($conn, $where);
   }
 
   public function getQueryApplicationClass() {
     return 'PhabricatorDivinerApplication';
   }
 
   public function getOrderableColumns() {
     return parent::getOrderableColumns() + array(
       'name' => array(
         'column' => 'name',
         'type' => 'string',
         'reverse' => true,
         'unique' => true,
       ),
     );
   }
 
   protected function newPagingMapFromPartialObject($object) {
     return array(
       'id' => (int)$object->getID(),
       'name' => $object->getName(),
     );
   }
 
   public function getBuiltinOrders() {
     return array(
       'name' => array(
         'vector' => array('name'),
         'name' => pht('Name'),
       ),
     ) + parent::getBuiltinOrders();
   }
 
 }
diff --git a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php
index 6c9cf1b356..169becaf0b 100644
--- a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php
+++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php
@@ -1,654 +1,655 @@
 <?php
 
 final class PhabricatorMailEmailEngine
   extends PhabricatorMailMessageEngine {
 
   public function newMessage() {
     $mailer = $this->getMailer();
     $mail = $this->getMail();
 
     $message = new PhabricatorMailEmailMessage();
 
     $from_address = $this->newFromEmailAddress();
     $message->setFromAddress($from_address);
 
     $reply_address = $this->newReplyToEmailAddress();
     if ($reply_address) {
       $message->setReplyToAddress($reply_address);
     }
 
     $to_addresses = $this->newToEmailAddresses();
     $cc_addresses = $this->newCCEmailAddresses();
 
     if (!$to_addresses && !$cc_addresses) {
       $mail->setMessage(
         pht(
           'Message has no valid recipients: all To/CC are disabled, '.
           'invalid, or configured not to receive this mail.'));
       return null;
     }
 
     // If this email describes a mail processing error, we rate limit outbound
     // messages to each individual address. This prevents messes where
     // something is stuck in a loop or dumps a ton of messages on us suddenly.
     if ($mail->getIsErrorEmail()) {
       $all_recipients = array();
       foreach ($to_addresses as $to_address) {
         $all_recipients[] = $to_address->getAddress();
       }
       foreach ($cc_addresses as $cc_address) {
         $all_recipients[] = $cc_address->getAddress();
       }
       if ($this->shouldRateLimitMail($all_recipients)) {
         $mail->setMessage(
           pht(
             'This is an error email, but one or more recipients have '.
             'exceeded the error email rate limit. Declining to deliver '.
             'message.'));
         return null;
       }
     }
 
     // Some mailers require a valid "To:" in order to deliver mail. If we
     // don't have any "To:", try to fill it in with a placeholder "To:".
     // If that also fails, move the "Cc:" line to "To:".
     if (!$to_addresses) {
       $void_address = $this->newVoidEmailAddress();
       $to_addresses = array($void_address);
     }
 
     $to_addresses = $this->getUniqueEmailAddresses($to_addresses);
     $cc_addresses = $this->getUniqueEmailAddresses(
       $cc_addresses,
       $to_addresses);
 
     $message->setToAddresses($to_addresses);
     $message->setCCAddresses($cc_addresses);
 
     $attachments = $this->newEmailAttachments();
     $message->setAttachments($attachments);
 
     $subject = $this->newEmailSubject();
     $message->setSubject($subject);
 
     $headers = $this->newEmailHeaders();
     foreach ($this->newEmailThreadingHeaders($mailer) as $threading_header) {
       $headers[] = $threading_header;
     }
 
     $stamps = $mail->getMailStamps();
     if ($stamps) {
       $headers[] = $this->newEmailHeader(
         'X-Phabricator-Stamps',
         implode(' ', $stamps));
     }
 
     $must_encrypt = $mail->getMustEncrypt();
 
     $raw_body = $mail->getBody();
     $body = $raw_body;
     if ($must_encrypt) {
       $parts = array();
 
       $encrypt_uri = $mail->getMustEncryptURI();
-      if (!strlen($encrypt_uri)) {
+      if ($encrypt_uri === null || !strlen($encrypt_uri)) {
         $encrypt_phid = $mail->getRelatedPHID();
         if ($encrypt_phid) {
           $encrypt_uri = urisprintf(
             '/object/%s/',
             $encrypt_phid);
         }
       }
 
-      if (strlen($encrypt_uri)) {
+      if ($encrypt_uri !== null && strlen($encrypt_uri)) {
         $parts[] = pht(
           'This secure message is notifying you of a change to this object:');
         $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri);
       }
 
       $parts[] = pht(
         'The content for this message can only be transmitted over a '.
         'secure channel. To view the message content, follow this '.
         'link:');
 
       $parts[] = PhabricatorEnv::getProductionURI($mail->getURI());
 
       $body = implode("\n\n", $parts);
     } else {
       $body = $raw_body;
     }
 
     $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
 
     $body = phutil_string_cast($body);
     if (strlen($body) > $body_limit) {
       $body = id(new PhutilUTF8StringTruncator())
         ->setMaximumBytes($body_limit)
         ->truncateString($body);
       $body .= "\n";
       $body .= pht('(This email was truncated at %d bytes.)', $body_limit);
     }
     $message->setTextBody($body);
     $body_limit -= strlen($body);
 
     // If we sent a different message body than we were asked to, record
     // what we actually sent to make debugging and diagnostics easier.
     if ($body !== $raw_body) {
       $mail->setDeliveredBody($body);
     }
 
     if ($must_encrypt) {
       $send_html = false;
     } else {
       $send_html = $this->shouldSendHTML();
     }
 
     if ($send_html) {
       $html_body = $mail->getHTMLBody();
       if (phutil_nonempty_string($html_body)) {
         // NOTE: We just drop the entire HTML body if it won't fit. Safely
         // truncating HTML is hard, and we already have the text body to fall
         // back to.
         if (strlen($html_body) <= $body_limit) {
           $message->setHTMLBody($html_body);
           $body_limit -= strlen($html_body);
         }
       }
     }
 
     // Pass the headers to the mailer, then save the state so we can show
     // them in the web UI. If the mail must be encrypted, we remove headers
     // which are not on a strict whitelist to avoid disclosing information.
     $filtered_headers = $this->filterHeaders($headers, $must_encrypt);
     $message->setHeaders($filtered_headers);
 
     $mail->setUnfilteredHeaders($headers);
     $mail->setDeliveredHeaders($headers);
 
     if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
       $mail->setMessage(
         pht(
           'This software is running in silent mode. See `%s` '.
           'in the configuration to change this setting.',
           'phabricator.silent'));
 
       return null;
     }
 
     return $message;
   }
 
 /* -(  Message Components  )------------------------------------------------- */
 
   private function newFromEmailAddress() {
     $from_address = $this->newDefaultEmailAddress();
     $mail = $this->getMail();
 
     // If the mail content must be encrypted, always disguise the sender.
     $must_encrypt = $mail->getMustEncrypt();
     if ($must_encrypt) {
       return $from_address;
     }
 
     // If we have a raw "From" address, use that.
     $raw_from = $mail->getRawFrom();
     if ($raw_from) {
       list($from_email, $from_name) = $raw_from;
       return $this->newEmailAddress($from_email, $from_name);
     }
 
     // Otherwise, use as much of the information for any sending entity as
     // we can.
     $from_phid = $mail->getFrom();
 
     $actor = $this->getActor($from_phid);
     if ($actor) {
       $actor_email = $actor->getEmailAddress();
       $actor_name = $actor->getName();
     } else {
       $actor_email = null;
       $actor_name = null;
     }
 
     $send_as_user = PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
     if ($send_as_user) {
       if ($actor_email !== null) {
         $from_address->setAddress($actor_email);
       }
     }
 
     if ($actor_name !== null) {
       $from_address->setDisplayName($actor_name);
     }
 
     return $from_address;
   }
 
   private function newReplyToEmailAddress() {
     $mail = $this->getMail();
 
     $reply_raw = $mail->getReplyTo();
     if (!phutil_nonempty_string($reply_raw)) {
       return null;
     }
 
     $reply_address = new PhutilEmailAddress($reply_raw);
 
     // If we have a sending object, change the display name.
     $from_phid = $mail->getFrom();
     $actor = $this->getActor($from_phid);
     if ($actor) {
       $reply_address->setDisplayName($actor->getName());
     }
 
     // If we don't have a display name, fill in a default.
-    if (!strlen($reply_address->getDisplayName())) {
+    $reply_display_name = $reply_address->getDisplayName();
+    if ($reply_display_name === null || !strlen($reply_display_name)) {
       $reply_address->setDisplayName(PlatformSymbols::getPlatformServerName());
     }
 
     return $reply_address;
   }
 
   private function newToEmailAddresses() {
     $mail = $this->getMail();
 
     $phids = $mail->getToPHIDs();
     $addresses = $this->newEmailAddressesFromActorPHIDs($phids);
 
     foreach ($mail->getRawToAddresses() as $raw_address) {
       $addresses[] = new PhutilEmailAddress($raw_address);
     }
 
     return $addresses;
   }
 
   private function newCCEmailAddresses() {
     $mail = $this->getMail();
     $phids = $mail->getCcPHIDs();
     return $this->newEmailAddressesFromActorPHIDs($phids);
   }
 
   private function newEmailAddressesFromActorPHIDs(array $phids) {
     $mail = $this->getMail();
     $phids = $mail->expandRecipients($phids);
 
     $addresses = array();
     foreach ($phids as $phid) {
       $actor = $this->getActor($phid);
       if (!$actor) {
         continue;
       }
 
       if (!$actor->isDeliverable()) {
         continue;
       }
 
       $addresses[] = new PhutilEmailAddress($actor->getEmailAddress());
     }
 
     return $addresses;
   }
 
   private function newEmailSubject() {
     $mail = $this->getMail();
 
     $is_threaded = (bool)$mail->getThreadID();
     $must_encrypt = $mail->getMustEncrypt();
 
     $subject = array();
 
     if ($is_threaded) {
       if ($this->shouldAddRePrefix()) {
         $subject[] = 'Re:';
       }
     }
 
     $subject_prefix = $mail->getSubjectPrefix();
     $subject_prefix = phutil_string_cast($subject_prefix);
     $subject_prefix = trim($subject_prefix);
 
     $subject[] = $subject_prefix;
 
     // If mail content must be encrypted, we replace the subject with
     // a generic one.
     if ($must_encrypt) {
       $encrypt_subject = $mail->getMustEncryptSubject();
-      if (!strlen($encrypt_subject)) {
+      if ($encrypt_subject === null || !strlen($encrypt_subject)) {
         $encrypt_subject = pht('Object Updated');
       }
       $subject[] = $encrypt_subject;
     } else {
       $vary_prefix = $mail->getVarySubjectPrefix();
       if (phutil_nonempty_string($vary_prefix)) {
         if ($this->shouldVarySubject()) {
           $subject[] = $vary_prefix;
         }
       }
 
       $subject[] = $mail->getSubject();
     }
 
     foreach ($subject as $key => $part) {
       if (!phutil_nonempty_string($part)) {
         unset($subject[$key]);
       }
     }
 
     $subject = implode(' ', $subject);
     return $subject;
   }
 
   private function newEmailHeaders() {
     $mail = $this->getMail();
 
     $headers = array();
 
     $headers[] = $this->newEmailHeader(
       'X-Phabricator-Sent-This-Message',
       'Yes');
     $headers[] = $this->newEmailHeader(
       'X-Mail-Transport-Agent',
       'MetaMTA');
 
     // Some clients respect this to suppress OOF and other auto-responses.
     $headers[] = $this->newEmailHeader(
       'X-Auto-Response-Suppress',
       'All');
 
     $mailtags = $mail->getMailTags();
     if ($mailtags) {
       $tag_header = array();
       foreach ($mailtags as $mailtag) {
         $tag_header[] = '<'.$mailtag.'>';
       }
       $tag_header = implode(', ', $tag_header);
       $headers[] = $this->newEmailHeader(
         'X-Phabricator-Mail-Tags',
         $tag_header);
     }
 
     $value = $mail->getHeaders();
     foreach ($value as $pair) {
       list($header_key, $header_value) = $pair;
 
       // NOTE: If we have \n in a header, SES rejects the email.
       $header_value = str_replace("\n", ' ', $header_value);
       $headers[] = $this->newEmailHeader($header_key, $header_value);
     }
 
     $is_bulk = $mail->getIsBulk();
     if ($is_bulk) {
       $headers[] = $this->newEmailHeader('Precedence', 'bulk');
     }
 
     if ($mail->getMustEncrypt()) {
       $headers[] = $this->newEmailHeader('X-Phabricator-Must-Encrypt', 'Yes');
     }
 
     $related_phid = $mail->getRelatedPHID();
     if ($related_phid) {
       $headers[] = $this->newEmailHeader('Thread-Topic', $related_phid);
     }
 
     $headers[] = $this->newEmailHeader(
       'X-Phabricator-Mail-ID',
       $mail->getID());
 
     $unique = Filesystem::readRandomCharacters(16);
     $headers[] = $this->newEmailHeader(
       'X-Phabricator-Send-Attempt',
       $unique);
 
     return $headers;
   }
 
   private function newEmailThreadingHeaders() {
     $mailer = $this->getMailer();
     $mail = $this->getMail();
 
     $headers = array();
 
     $thread_id = $mail->getThreadID();
     if (!phutil_nonempty_string($thread_id)) {
       return $headers;
     }
 
     $is_first = $mail->getIsFirstMessage();
 
     // NOTE: Gmail freaks out about In-Reply-To and References which aren't in
     // the form "<string@domain.tld>"; this is also required by RFC 2822,
     // although some clients are more liberal in what they accept.
     $domain = $this->newMailDomain();
     $thread_id = '<'.$thread_id.'@'.$domain.'>';
 
     if ($is_first && $mailer->supportsMessageIDHeader()) {
       $headers[] = $this->newEmailHeader('Message-ID',  $thread_id);
     } else {
       $in_reply_to = $thread_id;
       $references = array($thread_id);
       $parent_id = $mail->getParentMessageID();
       if ($parent_id) {
         $in_reply_to = $parent_id;
         // By RFC 2822, the most immediate parent should appear last
         // in the "References" header, so this order is intentional.
         $references[] = $parent_id;
       }
       $references = implode(' ', $references);
       $headers[] = $this->newEmailHeader('In-Reply-To', $in_reply_to);
       $headers[] = $this->newEmailHeader('References',  $references);
     }
     $thread_index = $this->generateThreadIndex($thread_id, $is_first);
     $headers[] = $this->newEmailHeader('Thread-Index', $thread_index);
 
     return $headers;
   }
 
   private function newEmailAttachments() {
     $mail = $this->getMail();
 
     // If the mail content must be encrypted, don't add attachments.
     $must_encrypt = $mail->getMustEncrypt();
     if ($must_encrypt) {
       return array();
     }
 
     return $mail->getAttachments();
   }
 
 /* -(  Preferences  )-------------------------------------------------------- */
 
   private function shouldAddRePrefix() {
     $preferences = $this->getPreferences();
 
     $value = $preferences->getSettingValue(
       PhabricatorEmailRePrefixSetting::SETTINGKEY);
 
     return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX);
   }
 
   private function shouldVarySubject() {
     $preferences = $this->getPreferences();
 
     $value = $preferences->getSettingValue(
       PhabricatorEmailVarySubjectsSetting::SETTINGKEY);
 
     return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS);
   }
 
   private function shouldSendHTML() {
     $preferences = $this->getPreferences();
 
     $value = $preferences->getSettingValue(
       PhabricatorEmailFormatSetting::SETTINGKEY);
 
     return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL);
   }
 
 
 /* -(  Utilities  )---------------------------------------------------------- */
 
   private function newEmailHeader($name, $value) {
     return id(new PhabricatorMailHeader())
       ->setName($name)
       ->setValue($value);
   }
 
   private function newEmailAddress($address, $name = null) {
     $object = id(new PhutilEmailAddress())
       ->setAddress($address);
 
-    if (strlen($name)) {
+    if ($name !== null && strlen($name)) {
       $object->setDisplayName($name);
     }
 
     return $object;
   }
 
   public function newDefaultEmailAddress() {
     $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address');
 
-    if (!strlen($raw_address)) {
+    if ($raw_address == null || !strlen($raw_address)) {
       $domain = $this->newMailDomain();
       $raw_address = "noreply@{$domain}";
     }
 
     $address = new PhutilEmailAddress($raw_address);
 
     if (!phutil_nonempty_string($address->getDisplayName())) {
       $address->setDisplayName(PlatformSymbols::getPlatformServerName());
     }
 
     return $address;
   }
 
   public function newVoidEmailAddress() {
     return $this->newDefaultEmailAddress();
   }
 
   private function newMailDomain() {
     $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
-    if (strlen($domain)) {
+    if ($domain !== null && strlen($domain)) {
       return $domain;
     }
 
     $install_uri = PhabricatorEnv::getURI('/');
     $install_uri = new PhutilURI($install_uri);
 
     return $install_uri->getDomain();
   }
 
   private function filterHeaders(array $headers, $must_encrypt) {
     assert_instances_of($headers, 'PhabricatorMailHeader');
 
     if (!$must_encrypt) {
       return $headers;
     }
 
     $whitelist = array(
       'In-Reply-To',
       'Message-ID',
       'Precedence',
       'References',
       'Thread-Index',
       'Thread-Topic',
 
       'X-Mail-Transport-Agent',
       'X-Auto-Response-Suppress',
 
       'X-Phabricator-Sent-This-Message',
       'X-Phabricator-Must-Encrypt',
       'X-Phabricator-Mail-ID',
       'X-Phabricator-Send-Attempt',
     );
 
     // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags".
     // This header contains a significant amount of meaningful information
     // about the object.
 
     $whitelist_map = array();
     foreach ($whitelist as $term) {
       $whitelist_map[phutil_utf8_strtolower($term)] = true;
     }
 
     foreach ($headers as $key => $header) {
       $name = $header->getName();
       $name = phutil_utf8_strtolower($name);
 
       if (!isset($whitelist_map[$name])) {
         unset($headers[$key]);
       }
     }
 
     return $headers;
   }
 
   private function getUniqueEmailAddresses(
     array $addresses,
     array $exclude = array()) {
     assert_instances_of($addresses, 'PhutilEmailAddress');
     assert_instances_of($exclude, 'PhutilEmailAddress');
 
     $seen = array();
 
     foreach ($exclude as $address) {
       $seen[$address->getAddress()] = true;
     }
 
     foreach ($addresses as $key => $address) {
       $raw_address = $address->getAddress();
 
       if (isset($seen[$raw_address])) {
         unset($addresses[$key]);
         continue;
       }
 
       $seen[$raw_address] = true;
     }
 
     return array_values($addresses);
   }
 
   private function generateThreadIndex($seed, $is_first_mail) {
     // When threading, Outlook ignores the 'References' and 'In-Reply-To'
     // headers that most clients use. Instead, it uses a custom 'Thread-Index'
     // header. The format of this header is something like this (from
     // camel-exchange-folder.c in Evolution Exchange):
 
     /* A new post to a folder gets a 27-byte-long thread index. (The value
      * is apparently unique but meaningless.) Each reply to a post gets a
      * 32-byte-long thread index whose first 27 bytes are the same as the
      * parent's thread index. Each reply to any of those gets a
      * 37-byte-long thread index, etc. The Thread-Index header contains a
      * base64 representation of this value.
      */
 
     // The specific implementation uses a 27-byte header for the first email
     // a recipient receives, and a random 5-byte suffix (32 bytes total)
     // thereafter. This means that all the replies are (incorrectly) siblings,
     // but it would be very difficult to keep track of the entire tree and this
     // gets us reasonable client behavior.
 
     $base = substr(md5($seed), 0, 27);
     if (!$is_first_mail) {
       // Not totally sure, but it seems like outlook orders replies by
       // thread-index rather than timestamp, so to get these to show up in the
       // right order we use the time as the last 4 bytes.
       $base .= ' '.pack('N', time());
     }
 
     return base64_encode($base);
   }
 
   private function shouldRateLimitMail(array $all_recipients) {
     try {
       PhabricatorSystemActionEngine::willTakeAction(
         $all_recipients,
         new PhabricatorMetaMTAErrorMailAction(),
         1);
       return false;
     } catch (PhabricatorSystemActionRateLimitException $ex) {
       return true;
     }
   }
 
 }
diff --git a/src/applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php b/src/applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php
index 5fb1209885..d989478470 100644
--- a/src/applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php
+++ b/src/applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php
@@ -1,20 +1,20 @@
 <?php
 
 final class PhabricatorMetaMTAReceivedMailProcessingException
   extends Exception {
 
   private $statusCode;
 
   public function getStatusCode() {
     return $this->statusCode;
   }
 
   public function __construct($status_code /* ... */) {
     $args = func_get_args();
     $this->statusCode = $args[0];
 
     $args = array_slice($args, 1);
-    call_user_func_array(array('parent', '__construct'), $args);
+    call_user_func_array(array(parent::class, '__construct'), $args);
   }
 
 }
diff --git a/src/applications/passphrase/view/PassphraseCredentialControl.php b/src/applications/passphrase/view/PassphraseCredentialControl.php
index 8ba1ef9bc8..2071482003 100644
--- a/src/applications/passphrase/view/PassphraseCredentialControl.php
+++ b/src/applications/passphrase/view/PassphraseCredentialControl.php
@@ -1,207 +1,209 @@
 <?php
 
 final class PassphraseCredentialControl extends AphrontFormControl {
 
   private $options = array();
   private $credentialType;
   private $defaultUsername;
   private $allowNull;
 
   public function setAllowNull($allow_null) {
     $this->allowNull = $allow_null;
     return $this;
   }
 
   public function setDefaultUsername($default_username) {
     $this->defaultUsername = $default_username;
     return $this;
   }
 
   public function setCredentialType($credential_type) {
     $this->credentialType = $credential_type;
     return $this;
   }
 
   public function getCredentialType() {
     return $this->credentialType;
   }
 
   public function setOptions(array $options) {
     assert_instances_of($options, 'PassphraseCredential');
     $this->options = $options;
     return $this;
   }
 
   protected function getCustomControlClass() {
     return 'passphrase-credential-control';
   }
 
   protected function renderInput() {
 
     $options_map = array();
     foreach ($this->options as $option) {
       $options_map[$option->getPHID()] = pht(
         '%s %s',
         $option->getMonogram(),
         $option->getName());
     }
 
     // The user editing the form may not have permission to see the current
     // credential. Populate it into the menu to allow them to save the form
     // without making any changes.
     $current_phid = $this->getValue();
-    if (strlen($current_phid) && empty($options_map[$current_phid])) {
+    if ($current_phid !== null && strlen($current_phid)
+      && empty($options_map[$current_phid])) {
+
       $viewer = $this->getViewer();
 
       $current_name = null;
       try {
         $user_credential = id(new PassphraseCredentialQuery())
           ->setViewer($viewer)
           ->withPHIDs(array($current_phid))
           ->executeOne();
 
         if ($user_credential) {
           $current_name = pht(
             '%s %s',
             $user_credential->getMonogram(),
             $user_credential->getName());
         }
       } catch (PhabricatorPolicyException $policy_exception) {
         // Pull the credential with the omnipotent viewer so we can look up
         // the ID and provide the monogram.
         $omnipotent_credential = id(new PassphraseCredentialQuery())
           ->setViewer(PhabricatorUser::getOmnipotentUser())
           ->withPHIDs(array($current_phid))
           ->executeOne();
         if ($omnipotent_credential) {
           $current_name = pht(
             '%s (Restricted Credential)',
             $omnipotent_credential->getMonogram());
         }
       }
 
       if ($current_name === null) {
         $current_name = pht(
           'Invalid Credential ("%s")',
           $current_phid);
       }
 
       $options_map = array(
         $current_phid => $current_name,
       ) + $options_map;
     }
 
 
     $disabled = $this->getDisabled();
     if ($this->allowNull) {
       $options_map = array('' => pht('(No Credentials)')) + $options_map;
     } else {
       if (!$options_map) {
         $options_map[''] = pht('(No Existing Credentials)');
         $disabled = true;
       }
     }
 
     Javelin::initBehavior('passphrase-credential-control');
 
     $options = AphrontFormSelectControl::renderSelectTag(
       $this->getValue(),
       $options_map,
       array(
         'id' => $this->getControlID(),
         'name' => $this->getName(),
         'disabled' => $disabled ? 'disabled' : null,
         'sigil' => 'passphrase-credential-select',
       ));
 
     if ($this->credentialType) {
       $button = javelin_tag(
         'a',
         array(
           'href' => '#',
           'class' => 'button button-grey mll',
           'sigil' => 'passphrase-credential-add',
           'mustcapture' => true,
           'style' => 'height: 20px;', // move aphront-form to tables
         ),
         pht('Add New Credential'));
     } else {
       $button = null;
     }
 
     return javelin_tag(
       'div',
       array(
         'sigil' => 'passphrase-credential-control',
         'meta' => array(
           'type' => $this->getCredentialType(),
           'username' => $this->defaultUsername,
           'allowNull' => $this->allowNull,
         ),
       ),
       array(
         $options,
         $button,
       ));
   }
 
   /**
    * Verify that a given actor has permission to use all of the credentials
    * in a list of credential transactions.
    *
    * In general, the rule here is:
    *
    *   - If you're editing an object and it uses a credential you can't use,
    *     that's fine as long as you don't change the credential.
    *   - If you do change the credential, the new credential must be one you
    *     can use.
    *
    * @param PhabricatorUser The acting user.
    * @param list<PhabricatorApplicationTransaction> List of credential altering
    *        transactions.
    * @return bool True if the transactions are valid.
    */
   public static function validateTransactions(
     PhabricatorUser $actor,
     array $xactions) {
 
     $new_phids = array();
     foreach ($xactions as $xaction) {
       $new = $xaction->getNewValue();
       if (!$new) {
         // Removing a credential, so this is OK.
         continue;
       }
 
       $old = $xaction->getOldValue();
       if ($old == $new) {
         // This is a no-op transaction, so this is also OK.
         continue;
       }
 
       // Otherwise, we need to check this credential.
       $new_phids[] = $new;
     }
 
     if (!$new_phids) {
       // No new credentials being set, so this is fine.
       return true;
     }
 
     $usable_credentials = id(new PassphraseCredentialQuery())
       ->setViewer($actor)
       ->withPHIDs($new_phids)
       ->execute();
     $usable_credentials = mpull($usable_credentials, null, 'getPHID');
 
     foreach ($new_phids as $phid) {
       if (empty($usable_credentials[$phid])) {
         return false;
       }
     }
 
     return true;
   }
 
 
 }
diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php
index 95507d61dc..a8ba53ae15 100644
--- a/src/applications/phame/storage/PhameBlog.php
+++ b/src/applications/phame/storage/PhameBlog.php
@@ -1,397 +1,397 @@
 <?php
 
 final class PhameBlog extends PhameDAO
   implements
     PhabricatorPolicyInterface,
     PhabricatorMarkupInterface,
     PhabricatorSubscribableInterface,
     PhabricatorFlaggableInterface,
     PhabricatorProjectInterface,
     PhabricatorDestructibleInterface,
     PhabricatorApplicationTransactionInterface,
     PhabricatorConduitResultInterface,
     PhabricatorFulltextInterface,
     PhabricatorFerretInterface {
 
   protected $name;
   protected $subtitle;
   protected $description;
   protected $domain;
   protected $domainFullURI;
   protected $parentSite;
   protected $parentDomain;
   protected $configData;
   protected $creatorPHID;
   protected $viewPolicy;
   protected $editPolicy;
   protected $interactPolicy;
   protected $status;
   protected $mailKey;
   protected $profileImagePHID;
   protected $headerImagePHID;
 
   private $profileImageFile = self::ATTACHABLE;
   private $headerImageFile = self::ATTACHABLE;
 
   const STATUS_ACTIVE = 'active';
   const STATUS_ARCHIVED = 'archived';
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID   => true,
       self::CONFIG_SERIALIZATION => array(
         'configData' => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'name' => 'text64',
         'subtitle' => 'text64',
         'description' => 'text',
         'domain' => 'text128?',
         'domainFullURI' => 'text128?',
         'parentSite' => 'text128?',
         'parentDomain' => 'text128?',
         'status' => 'text32',
         'mailKey' => 'bytes20',
         'profileImagePHID' => 'phid?',
         'headerImagePHID' => 'phid?',
 
         'editPolicy' => 'policy',
         'viewPolicy' => 'policy',
         'interactPolicy' => 'policy',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_phid' => null,
         'phid' => array(
           'columns' => array('phid'),
           'unique' => true,
         ),
         'domain' => array(
           'columns' => array('domain'),
           'unique' => true,
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   public function save() {
     if (!$this->getMailKey()) {
       $this->setMailKey(Filesystem::readRandomCharacters(20));
     }
     return parent::save();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPhameBlogPHIDType::TYPECONST);
   }
 
   public static function initializeNewBlog(PhabricatorUser $actor) {
     $blog = id(new PhameBlog())
       ->setCreatorPHID($actor->getPHID())
       ->setStatus(self::STATUS_ACTIVE)
       ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
       ->setEditPolicy(PhabricatorPolicies::POLICY_USER)
       ->setInteractPolicy(PhabricatorPolicies::POLICY_USER);
 
     return $blog;
   }
 
   public function isArchived() {
     return ($this->getStatus() == self::STATUS_ARCHIVED);
   }
 
   public static function getStatusNameMap() {
     return array(
       self::STATUS_ACTIVE => pht('Active'),
       self::STATUS_ARCHIVED => pht('Archived'),
     );
   }
 
   /**
    * Makes sure a given custom blog uri is properly configured in DNS
    * to point at this Phabricator instance. If there is an error in
    * the configuration, return a string describing the error and how
    * to fix it. If there is no error, return an empty string.
    *
    * @return string
    */
   public function validateCustomDomain($domain_full_uri) {
     $example_domain = 'http://blog.example.com/';
     $label = pht('Invalid');
 
     // note this "uri" should be pretty busted given the desired input
     // so just use it to test if there's a protocol specified
     $uri = new PhutilURI($domain_full_uri);
     $domain = $uri->getDomain();
     $protocol = $uri->getProtocol();
     $path = $uri->getPath();
     $supported_protocols = array('http', 'https');
 
     if (!in_array($protocol, $supported_protocols)) {
       return pht(
           'The custom domain should include a valid protocol in the URI '.
           '(for example, "%s"). Valid protocols are "http" or "https".',
           $example_domain);
     }
 
     if (strlen($path) && $path != '/') {
       return pht(
           'The custom domain should not specify a path (hosting a Phame '.
           'blog at a path is currently not supported). Instead, just provide '.
           'the bare domain name (for example, "%s").',
           $example_domain);
     }
 
     if (strpos($domain, '.') === false) {
       return pht(
           'The custom domain should contain at least one dot (.) because '.
           'some browsers fail to set cookies on domains without a dot. '.
           'Instead, use a normal looking domain name like "%s".',
           $example_domain);
     }
 
     if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
       $href = PhabricatorEnv::getProductionURI(
         '/config/edit/policy.allow-public/');
       return pht(
         'For custom domains to work, this this server must be '.
         'configured to allow the public access policy. Configure this '.
         'setting %s, or ask an administrator to configure this setting. '.
         'The domain can be specified later once this setting has been '.
         'changed.',
         phutil_tag(
           'a',
           array('href' => $href),
           pht('here')));
     }
 
     return null;
   }
 
   public function getLiveURI() {
-    if (strlen($this->getDomain())) {
+    if ($this->getDomain() !== null && strlen($this->getDomain())) {
       return $this->getExternalLiveURI();
     } else {
       return $this->getInternalLiveURI();
     }
   }
 
   public function getExternalLiveURI() {
     $uri = new PhutilURI($this->getDomainFullURI());
     PhabricatorEnv::requireValidRemoteURIForLink($uri);
     return (string)$uri;
   }
 
   public function getExternalParentURI() {
     $uri = $this->getParentDomain();
     PhabricatorEnv::requireValidRemoteURIForLink($uri);
     return (string)$uri;
   }
 
   public function getInternalLiveURI() {
     return '/phame/live/'.$this->getID().'/';
   }
 
   public function getViewURI() {
     return '/phame/blog/view/'.$this->getID().'/';
   }
 
   public function getManageURI() {
     return '/phame/blog/manage/'.$this->getID().'/';
   }
 
   public function getProfileImageURI() {
     return $this->getProfileImageFile()->getBestURI();
   }
 
   public function attachProfileImageFile(PhabricatorFile $file) {
     $this->profileImageFile = $file;
     return $this;
   }
 
   public function getProfileImageFile() {
     return $this->assertAttached($this->profileImageFile);
   }
 
   public function getHeaderImageURI() {
     return $this->getHeaderImageFile()->getBestURI();
   }
 
   public function attachHeaderImageFile(PhabricatorFile $file) {
     $this->headerImageFile = $file;
     return $this;
   }
 
   public function getHeaderImageFile() {
     return $this->assertAttached($this->headerImageFile);
   }
 
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */
 
 
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
       PhabricatorPolicyCapability::CAN_INTERACT,
       PhabricatorPolicyCapability::CAN_EDIT,
     );
   }
 
 
   public function getPolicy($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return $this->getViewPolicy();
       case PhabricatorPolicyCapability::CAN_INTERACT:
         return $this->getInteractPolicy();
       case PhabricatorPolicyCapability::CAN_EDIT:
         return $this->getEditPolicy();
     }
   }
 
   public function hasAutomaticCapability($capability, PhabricatorUser $user) {
     $can_edit = PhabricatorPolicyCapability::CAN_EDIT;
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         // Users who can edit or post to a blog can always view it.
         if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
           return true;
         }
         break;
     }
 
     return false;
   }
 
 
   public function describeAutomaticCapability($capability) {
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
         return pht(
           'Users who can edit a blog can always view it.');
     }
 
     return null;
   }
 
 
 /* -(  PhabricatorMarkupInterface Implementation  )-------------------------- */
 
 
   public function getMarkupFieldKey($field) {
     $content = $this->getMarkupText($field);
     return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
   }
 
 
   public function newMarkupEngine($field) {
     return PhabricatorMarkupEngine::newPhameMarkupEngine();
   }
 
 
   public function getMarkupText($field) {
     return $this->getDescription();
   }
 
 
   public function didMarkupText(
     $field,
     $output,
     PhutilMarkupEngine $engine) {
     return $output;
   }
 
   public function shouldUseMarkupCache($field) {
     return (bool)$this->getPHID();
   }
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
   public function destroyObjectPermanently(
     PhabricatorDestructionEngine $engine) {
 
     $this->openTransaction();
 
       $posts = id(new PhamePostQuery())
         ->setViewer($engine->getViewer())
         ->withBlogPHIDs(array($this->getPHID()))
         ->execute();
       foreach ($posts as $post) {
         $engine->destroyObject($post);
       }
       $this->delete();
 
     $this->saveTransaction();
   }
 
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */
 
 
   public function getApplicationTransactionEditor() {
     return new PhameBlogEditor();
   }
 
   public function getApplicationTransactionTemplate() {
     return new PhameBlogTransaction();
   }
 
 
 /* -(  PhabricatorSubscribableInterface Implementation  )-------------------- */
 
 
   public function isAutomaticallySubscribed($phid) {
     return false;
   }
 
 
 /* -(  PhabricatorConduitResultInterface  )---------------------------------- */
 
 
   public function getFieldSpecificationsForConduit() {
     return array(
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('name')
         ->setType('string')
         ->setDescription(pht('The name of the blog.')),
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('description')
         ->setType('string')
         ->setDescription(pht('Blog description.')),
       id(new PhabricatorConduitSearchFieldSpecification())
         ->setKey('status')
         ->setType('string')
         ->setDescription(pht('Archived or active status.')),
     );
   }
 
   public function getFieldValuesForConduit() {
     return array(
       'name' => $this->getName(),
       'description' => $this->getDescription(),
       'status' => $this->getStatus(),
     );
   }
 
   public function getConduitSearchAttachments() {
     return array();
   }
 
 
 /* -(  PhabricatorFulltextInterface  )--------------------------------------- */
 
   public function newFulltextEngine() {
     return new PhameBlogFulltextEngine();
   }
 
 
 /* -(  PhabricatorFerretInterface  )----------------------------------------- */
 
 
   public function newFerretEngine() {
     return new PhameBlogFerretEngine();
   }
 
 }
diff --git a/src/applications/pholio/controller/PholioImageUploadController.php b/src/applications/pholio/controller/PholioImageUploadController.php
index 0ff5e061f5..44d7ea7eed 100644
--- a/src/applications/pholio/controller/PholioImageUploadController.php
+++ b/src/applications/pholio/controller/PholioImageUploadController.php
@@ -1,44 +1,44 @@
 <?php
 
 final class PholioImageUploadController extends PholioController {
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
 
     $phid = $request->getStr('filePHID');
     $replaces_phid = $request->getStr('replacesPHID');
     $title = $request->getStr('title');
     $description = $request->getStr('description');
 
     $file = id(new PhabricatorFileQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($phid))
       ->executeOne();
     if (!$file) {
       return new Aphront404Response();
     }
 
-    if (!strlen($title)) {
+    if (!phutil_nonempty_string($title)) {
       $title = $file->getName();
     }
 
     $image = PholioImage::initializeNewImage()
       ->setAuthorPHID($viewer->getPHID())
       ->attachFile($file)
       ->setName($title)
       ->setDescription($description)
       ->makeEphemeral();
 
     $view = id(new PholioUploadedImageView())
       ->setUser($viewer)
       ->setImage($image)
       ->setReplacesPHID($replaces_phid);
 
     $content = array(
       'markup' => $view,
     );
 
     return id(new AphrontAjaxResponse())->setContent($content);
   }
 
 }
diff --git a/src/applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php b/src/applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php
index d7fcbc2c7a..660e437942 100644
--- a/src/applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php
+++ b/src/applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php
@@ -1,62 +1,62 @@
 <?php
 
 final class PhabricatorSpacesNamespaceNameTransaction
   extends PhabricatorSpacesNamespaceTransactionType {
 
   const TRANSACTIONTYPE = 'spaces:name';
 
   public function generateOldValue($object) {
     return $object->getNamespaceName();
   }
 
   public function applyInternalEffects($object, $value) {
     $object->setNamespaceName($value);
   }
 
   public function getTitle() {
     $old = $this->getOldValue();
-    if (!strlen($old)) {
+    if ($old === null || !strlen($old)) {
       return pht(
         '%s created this space.',
         $this->renderAuthor());
     } else {
       return pht(
         '%s renamed this space from %s to %s.',
         $this->renderAuthor(),
         $this->renderOldValue(),
         $this->renderNewValue());
     }
   }
 
   public function getTitleForFeed() {
     return pht(
       '%s renamed space %s from %s to %s.',
       $this->renderAuthor(),
       $this->renderObject(),
       $this->renderOldValue(),
       $this->renderNewValue());
   }
 
   public function validateTransactions($object, array $xactions) {
     $errors = array();
 
     if ($this->isEmptyTextTransaction($object->getNamespaceName(), $xactions)) {
       $errors[] = $this->newRequiredError(
         pht('Spaces must have a name.'));
     }
 
     $max_length = $object->getColumnMaximumByteLength('namespaceName');
     foreach ($xactions as $xaction) {
       $new_value = $xaction->getNewValue();
       $new_length = strlen($new_value);
       if ($new_length > $max_length) {
         $errors[] = $this->newInvalidError(
           pht('The name can be no longer than %s characters.',
           new PhutilNumber($max_length)));
       }
     }
 
     return $errors;
   }
 
 }
diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
index 7d5e3c533e..dac362974a 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
@@ -1,497 +1,497 @@
 <?php
 
 abstract class PhabricatorModularTransactionType
   extends Phobject {
 
   private $storage;
   private $viewer;
   private $editor;
 
   final public function getTransactionTypeConstant() {
     return $this->getPhobjectClassConstant('TRANSACTIONTYPE');
   }
 
   public function generateOldValue($object) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function generateNewValue($object, $value) {
     return $value;
   }
 
   public function validateTransactions($object, array $xactions) {
     return array();
   }
 
   public function applyInternalEffects($object, $value) {
     return;
   }
 
   public function applyExternalEffects($object, $value) {
     return;
   }
 
   public function didCommitTransaction($object, $value) {
     return;
   }
 
   public function getTransactionHasEffect($object, $old, $new) {
     return ($old !== $new);
   }
 
   public function extractFilePHIDs($object, $value) {
     return array();
   }
 
   public function shouldHide() {
     return false;
   }
 
   public function shouldHideForFeed() {
     return false;
   }
 
   public function shouldHideForMail() {
     return false;
   }
 
   public function shouldHideForNotifications() {
     return null;
   }
 
   public function getIcon() {
     return null;
   }
 
   public function getTitle() {
     return null;
   }
 
   public function getTitleForFeed() {
     return null;
   }
 
   public function getActionName() {
     return null;
   }
 
   public function getActionStrength() {
     return null;
   }
 
   public function getColor() {
     return null;
   }
 
   public function hasChangeDetailView() {
     return false;
   }
 
   public function newChangeDetailView() {
     return null;
   }
 
   public function getMailDiffSectionHeader() {
     return pht('EDIT DETAILS');
   }
 
   public function newRemarkupChanges() {
     return array();
   }
 
   public function mergeTransactions(
     $object,
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
     return null;
   }
 
   final public function setStorage(
     PhabricatorApplicationTransaction $xaction) {
     $this->storage = $xaction;
     return $this;
   }
 
   private function getStorage() {
     return $this->storage;
   }
 
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   final protected function getViewer() {
     return $this->viewer;
   }
 
   final public function getActor() {
     return $this->getEditor()->getActor();
   }
 
   final public function getActingAsPHID() {
     return $this->getEditor()->getActingAsPHID();
   }
 
   final public function setEditor(
     PhabricatorApplicationTransactionEditor $editor) {
     $this->editor = $editor;
     return $this;
   }
 
   final protected function getEditor() {
     if (!$this->editor) {
       throw new PhutilInvalidStateException('setEditor');
     }
     return $this->editor;
   }
 
   final protected function hasEditor() {
     return (bool)$this->editor;
   }
 
   final protected function getAuthorPHID() {
     return $this->getStorage()->getAuthorPHID();
   }
 
   final protected function getObjectPHID() {
     return $this->getStorage()->getObjectPHID();
   }
 
   final protected function getObject() {
     return $this->getStorage()->getObject();
   }
 
   final protected function getOldValue() {
     return $this->getStorage()->getOldValue();
   }
 
   final protected function getNewValue() {
     return $this->getStorage()->getNewValue();
   }
 
   final protected function renderAuthor() {
     $author_phid = $this->getAuthorPHID();
     return $this->getStorage()->renderHandleLink($author_phid);
   }
 
   final protected function renderObject() {
     $object_phid = $this->getObjectPHID();
     return $this->getStorage()->renderHandleLink($object_phid);
   }
 
   final protected function renderHandle($phid) {
     $viewer = $this->getViewer();
     $display = $viewer->renderHandle($phid);
 
     if ($this->isTextMode()) {
       $display->setAsText(true);
     }
 
     return $display;
   }
 
   final protected function renderOldHandle() {
     return $this->renderHandle($this->getOldValue());
   }
 
   final protected function renderNewHandle() {
     return $this->renderHandle($this->getNewValue());
   }
 
   final protected function renderOldPolicy() {
     return $this->renderPolicy($this->getOldValue(), 'old');
   }
 
   final protected function renderNewPolicy() {
     return $this->renderPolicy($this->getNewValue(), 'new');
   }
 
   final protected function renderPolicy($phid, $mode) {
     $viewer = $this->getViewer();
     $handles = $viewer->loadHandles(array($phid));
 
     $policy = PhabricatorPolicy::newFromPolicyAndHandle(
       $phid,
       $handles[$phid]);
 
     $ref = $policy->newRef($viewer);
 
     if ($this->isTextMode()) {
       $name = $ref->getPolicyDisplayName();
     } else {
       $storage = $this->getStorage();
       $name = $ref->newTransactionLink($mode, $storage);
     }
 
     return $this->renderValue($name);
   }
 
   final protected function renderHandleList(array $phids) {
     $viewer = $this->getViewer();
     $display = $viewer->renderHandleList($phids)
       ->setAsInline(true);
 
     if ($this->isTextMode()) {
       $display->setAsText(true);
     }
 
     return $display;
   }
 
   final protected function renderValue($value) {
     if ($this->isTextMode()) {
       return sprintf('"%s"', $value);
     }
 
     return phutil_tag(
       'span',
       array(
         'class' => 'phui-timeline-value',
       ),
       $value);
   }
 
   final protected function renderValueList(array $values) {
     $result = array();
     foreach ($values as $value) {
       $result[] = $this->renderValue($value);
     }
 
     if ($this->isTextMode()) {
       return implode(', ', $result);
     }
 
     return phutil_implode_html(', ', $result);
   }
 
   final protected function renderOldValue() {
     return $this->renderValue($this->getOldValue());
   }
 
   final protected function renderNewValue() {
     return $this->renderValue($this->getNewValue());
   }
 
   final protected function renderDate($epoch) {
     $viewer = $this->getViewer();
 
     // We accept either epoch timestamps or dictionaries describing a
     // PhutilCalendarDateTime.
     if (is_array($epoch)) {
       $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch)
         ->setViewerTimezone($viewer->getTimezoneIdentifier());
 
       $all_day = $datetime->getIsAllDay();
 
       $epoch = $datetime->getEpoch();
     } else {
       $all_day = false;
     }
 
     if ($all_day) {
       $display = phabricator_date($epoch, $viewer);
     } else if ($this->isRenderingTargetExternal()) {
       // When rendering to text, we explicitly render the offset from UTC to
       // provide context to the date: the mail may be generating with the
       // server's settings, or the user may later refer back to it after
       // changing timezones.
 
       $display = phabricator_datetimezone($epoch, $viewer);
     } else {
       $display = phabricator_datetime($epoch, $viewer);
     }
 
     return $this->renderValue($display);
   }
 
   final protected function renderOldDate() {
     return $this->renderDate($this->getOldValue());
   }
 
   final protected function renderNewDate() {
     return $this->renderDate($this->getNewValue());
   }
 
   final protected function newError($title, $message, $xaction = null) {
     return new PhabricatorApplicationTransactionValidationError(
       $this->getTransactionTypeConstant(),
       $title,
       $message,
       $xaction);
   }
 
   final protected function newRequiredError($message, $xaction = null) {
     return $this->newError(pht('Required'), $message, $xaction)
       ->setIsMissingFieldError(true);
   }
 
   final protected function newInvalidError($message, $xaction = null) {
     return $this->newError(pht('Invalid'), $message, $xaction);
   }
 
   final protected function isNewObject() {
     return $this->getEditor()->getIsNewObject();
   }
 
   final protected function isEmptyTextTransaction($value, array $xactions) {
     foreach ($xactions as $xaction) {
       $value = $xaction->getNewValue();
     }
 
-    return !strlen($value);
+    return $value === null || !strlen($value);
   }
 
   /**
    * When rendering to external targets (Email/Asana/etc), we need to include
    * more information that users can't obtain later.
    */
   final protected function isRenderingTargetExternal() {
     // Right now, this is our best proxy for this:
     return $this->isTextMode();
     // "TARGET_TEXT" means "EMail" and "TARGET_HTML" means "Web".
   }
 
   final protected function isTextMode() {
     $target = $this->getStorage()->getRenderingTarget();
     return ($target == PhabricatorApplicationTransaction::TARGET_TEXT);
   }
 
   final protected function newRemarkupChange() {
     return id(new PhabricatorTransactionRemarkupChange())
       ->setTransaction($this->getStorage());
   }
 
   final protected function isCreateTransaction() {
     return $this->getStorage()->getIsCreateTransaction();
   }
 
   final protected function getPHIDList(array $old, array $new) {
     $editor = $this->getEditor();
 
     return $editor->getPHIDList($old, $new);
   }
 
   public function getMetadataValue($key, $default = null) {
     return $this->getStorage()->getMetadataValue($key, $default);
   }
 
   public function loadTransactionTypeConduitData(array $xactions) {
     return null;
   }
 
   public function getTransactionTypeForConduit($xaction) {
     return null;
   }
 
   public function getFieldValuesForConduit($xaction, $data) {
     return array();
   }
 
   protected function requireApplicationCapability($capability) {
     $application_class = $this->getEditor()->getEditorApplicationClass();
     $application = newv($application_class, array());
 
     PhabricatorPolicyFilter::requireCapability(
       $this->getActor(),
       $application,
       $capability);
   }
 
   /**
    * Get a list of capabilities the actor must have on the object to apply
    * a transaction to it.
    *
    * Usually, you should use this to reduce capability requirements when a
    * transaction (like leaving a Conpherence thread) can be applied without
    * having edit permission on the object. You can override this method to
    * remove the CAN_EDIT requirement, or to replace it with a different
    * requirement.
    *
    * If you are increasing capability requirements and need to add an
    * additional capability or policy requirement above and beyond CAN_EDIT, it
    * is usually better implemented as a validation check.
    *
    * @param object Object being edited.
    * @param PhabricatorApplicationTransaction Transaction being applied.
    * @return null|const|list<const> A capability constant (or list of
    *    capability constants) which the actor must have on the object. You can
    *    return `null` as a shorthand for "no capabilities are required".
    */
   public function getRequiredCapabilities(
     $object,
     PhabricatorApplicationTransaction $xaction) {
     return PhabricatorPolicyCapability::CAN_EDIT;
   }
 
   public function shouldTryMFA(
     $object,
     PhabricatorApplicationTransaction $xaction) {
     return false;
   }
 
   // NOTE: See T12921. These APIs are somewhat aspirational. For now, all of
   // these use "TARGET_TEXT" (even the HTML methods!) and the body methods
   // actually return Remarkup, not text or HTML.
 
   final public function getTitleForTextMail() {
     return $this->getTitleForMailWithRenderingTarget(
       PhabricatorApplicationTransaction::TARGET_TEXT);
   }
 
   final public function getTitleForHTMLMail() {
     return $this->getTitleForMailWithRenderingTarget(
       PhabricatorApplicationTransaction::TARGET_TEXT);
   }
 
   final public function getBodyForTextMail() {
     return $this->getBodyForMailWithRenderingTarget(
       PhabricatorApplicationTransaction::TARGET_TEXT);
   }
 
   final public function getBodyForHTMLMail() {
     return $this->getBodyForMailWithRenderingTarget(
       PhabricatorApplicationTransaction::TARGET_TEXT);
   }
 
   private function getTitleForMailWithRenderingTarget($target) {
     $storage = $this->getStorage();
 
     $old_target = $storage->getRenderingTarget();
     try {
       $storage->setRenderingTarget($target);
       $result = $this->getTitleForMail();
     } catch (Exception $ex) {
       $storage->setRenderingTarget($old_target);
       throw $ex;
     }
     $storage->setRenderingTarget($old_target);
 
     return $result;
   }
 
   private function getBodyForMailWithRenderingTarget($target) {
     $storage = $this->getStorage();
 
     $old_target = $storage->getRenderingTarget();
     try {
       $storage->setRenderingTarget($target);
       $result = $this->getBodyForMail();
     } catch (Exception $ex) {
       $storage->setRenderingTarget($old_target);
       throw $ex;
     }
     $storage->setRenderingTarget($old_target);
 
     return $result;
   }
 
   protected function getTitleForMail() {
     return false;
   }
 
   protected function getBodyForMail() {
     return false;
   }
 
 }
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php
index a96ebefda1..014927bcd1 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php
@@ -1,275 +1,275 @@
 <?php
 
 /**
  * Common code for standard field types which store lists of PHIDs.
  */
 abstract class PhabricatorStandardCustomFieldPHIDs
   extends PhabricatorStandardCustomField {
 
   public function buildFieldIndexes() {
     $indexes = array();
 
     $value = $this->getFieldValue();
     if (is_array($value)) {
       foreach ($value as $phid) {
         $indexes[] = $this->newStringIndex($phid);
       }
     }
 
     return $indexes;
   }
 
   public function readValueFromRequest(AphrontRequest $request) {
     $value = $request->getArr($this->getFieldKey());
     $this->setFieldValue($value);
   }
 
   public function getValueForStorage() {
     $value = $this->getFieldValue();
     if (!$value) {
       return null;
     }
 
     return json_encode(array_values($value));
   }
 
   public function setValueFromStorage($value) {
     // NOTE: We're accepting either a JSON string (a real storage value) or
     // an array (from HTTP parameter prefilling). This is a little hacky, but
     // should hold until this can get cleaned up more thoroughly.
     // TODO: Clean this up.
 
     $result = array();
-    if (!is_array($value)) {
+    if ($value !== null && !is_array($value)) {
       $value = json_decode($value, true);
       if (is_array($value)) {
         $result = array_values($value);
       }
     }
 
     $this->setFieldValue($value);
 
     return $this;
   }
 
   public function readApplicationSearchValueFromRequest(
     PhabricatorApplicationSearchEngine $engine,
     AphrontRequest $request) {
     return $request->getArr($this->getFieldKey());
   }
 
   public function applyApplicationSearchConstraintToQuery(
     PhabricatorApplicationSearchEngine $engine,
     PhabricatorCursorPagedPolicyAwareQuery $query,
     $value) {
     if ($value) {
       $query->withApplicationSearchContainsConstraint(
         $this->newStringIndex(null),
         $value);
     }
   }
 
   public function getRequiredHandlePHIDsForPropertyView() {
     $value = $this->getFieldValue();
     if ($value) {
       return $value;
     }
     return array();
   }
 
   public function renderPropertyViewValue(array $handles) {
     $value = $this->getFieldValue();
     if (!$value) {
       return null;
     }
 
     $handles = mpull($handles, 'renderHovercardLink');
     $handles = phutil_implode_html(', ', $handles);
     return $handles;
   }
 
   public function getRequiredHandlePHIDsForEdit() {
     $value = $this->getFieldValue();
     if ($value) {
       return $value;
     } else {
       return array();
     }
   }
 
   public function getApplicationTransactionRequiredHandlePHIDs(
     PhabricatorApplicationTransaction $xaction) {
 
     $old = $this->decodeValue($xaction->getOldValue());
     $new = $this->decodeValue($xaction->getNewValue());
 
     $add = array_diff($new, $old);
     $rem = array_diff($old, $new);
 
     return array_merge($add, $rem);
   }
 
   public function getApplicationTransactionTitle(
     PhabricatorApplicationTransaction $xaction) {
     $author_phid = $xaction->getAuthorPHID();
 
     $old = $this->decodeValue($xaction->getOldValue());
     $new = $this->decodeValue($xaction->getNewValue());
 
     $add = array_diff($new, $old);
     $rem = array_diff($old, $new);
 
     if ($add && !$rem) {
       return pht(
         '%s updated %s, added %d: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         phutil_count($add),
         $xaction->renderHandleList($add));
     } else if ($rem && !$add) {
       return pht(
         '%s updated %s, removed %s: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         phutil_count($rem),
         $xaction->renderHandleList($rem));
     } else {
       return pht(
         '%s updated %s, added %s: %s; removed %s: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         phutil_count($add),
         $xaction->renderHandleList($add),
         phutil_count($rem),
         $xaction->renderHandleList($rem));
     }
   }
 
   public function getApplicationTransactionTitleForFeed(
     PhabricatorApplicationTransaction $xaction) {
     $author_phid = $xaction->getAuthorPHID();
     $object_phid = $xaction->getObjectPHID();
 
     $old = $this->decodeValue($xaction->getOldValue());
     $new = $this->decodeValue($xaction->getNewValue());
 
     $add = array_diff($new, $old);
     $rem = array_diff($old, $new);
 
     if ($add && !$rem) {
       return pht(
         '%s updated %s for %s, added %d: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         $xaction->renderHandleLink($object_phid),
         phutil_count($add),
         $xaction->renderHandleList($add));
     } else if ($rem && !$add) {
       return pht(
         '%s updated %s for %s, removed %s: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         $xaction->renderHandleLink($object_phid),
         phutil_count($rem),
         $xaction->renderHandleList($rem));
     } else {
       return pht(
         '%s updated %s for %s, added %s: %s; removed %s: %s.',
         $xaction->renderHandleLink($author_phid),
         $this->getFieldName(),
         $xaction->renderHandleLink($object_phid),
         phutil_count($add),
         $xaction->renderHandleList($add),
         phutil_count($rem),
         $xaction->renderHandleList($rem));
     }
   }
 
   public function validateApplicationTransactions(
     PhabricatorApplicationTransactionEditor $editor,
     $type,
     array $xactions) {
 
     $errors = parent::validateApplicationTransactions(
       $editor,
       $type,
       $xactions);
 
     // If the user is adding PHIDs, make sure the new PHIDs are valid and
     // visible to the actor. It's OK for a user to edit a field which includes
     // some invalid or restricted values, but they can't add new ones.
 
     foreach ($xactions as $xaction) {
       $old = $this->decodeValue($xaction->getOldValue());
       $new = $this->decodeValue($xaction->getNewValue());
 
       $add = array_diff($new, $old);
 
       $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer(
         $editor->getActor(),
         $add);
 
       if ($invalid) {
         $error = new PhabricatorApplicationTransactionValidationError(
           $type,
           pht('Invalid'),
           pht(
             'Some of the selected PHIDs in field "%s" are invalid or '.
             'restricted: %s.',
             $this->getFieldName(),
             implode(', ', $invalid)),
           $xaction);
         $errors[] = $error;
         $this->setFieldError(pht('Invalid'));
       }
     }
 
     return $errors;
   }
 
   public function shouldAppearInHerald() {
     return true;
   }
 
   public function getHeraldFieldConditions() {
     return array(
       HeraldAdapter::CONDITION_INCLUDE_ALL,
       HeraldAdapter::CONDITION_INCLUDE_ANY,
       HeraldAdapter::CONDITION_INCLUDE_NONE,
       HeraldAdapter::CONDITION_EXISTS,
       HeraldAdapter::CONDITION_NOT_EXISTS,
     );
   }
 
   public function getHeraldFieldStandardType() {
     return HeraldField::STANDARD_PHID_NULLABLE;
   }
 
   public function getHeraldFieldValue() {
     // If the field has a `null` value, make sure we hand an `array()` to
     // Herald.
     $value = parent::getHeraldFieldValue();
     if ($value) {
       return $value;
     }
     return array();
   }
 
   protected function decodeValue($value) {
     if ($value === null) {
       return array();
     }
 
     $value = json_decode($value);
     if (!is_array($value)) {
       $value = array();
     }
 
     return $value;
   }
 
   protected function getHTTPParameterType() {
     return new AphrontPHIDListHTTPParameterType();
   }
 
 }