diff --git a/src/applications/calendar/parser/ics/PhutilICSWriter.php b/src/applications/calendar/parser/ics/PhutilICSWriter.php
index 8e7d06804c..c35008e079 100644
--- a/src/applications/calendar/parser/ics/PhutilICSWriter.php
+++ b/src/applications/calendar/parser/ics/PhutilICSWriter.php
@@ -1,391 +1,391 @@
 <?php
 
 final class PhutilICSWriter extends Phobject {
 
   public function writeICSDocument(PhutilCalendarRootNode $node) {
     $out = array();
 
     foreach ($node->getChildren() as $child) {
       $out[] = $this->writeNode($child);
     }
 
     return implode('', $out);
   }
 
   private function writeNode(PhutilCalendarNode $node) {
     if (!$this->getICSNodeType($node)) {
       return null;
     }
 
     $out = array();
 
     $out[] = $this->writeBeginNode($node);
     $out[] = $this->writeNodeProperties($node);
 
     if ($node instanceof PhutilCalendarContainerNode) {
       foreach ($node->getChildren() as $child) {
         $out[] = $this->writeNode($child);
       }
     }
 
     $out[] = $this->writeEndNode($node);
 
     return implode('', $out);
   }
 
   private function writeBeginNode(PhutilCalendarNode $node) {
     $type = $this->getICSNodeType($node);
     return $this->wrapICSLine("BEGIN:{$type}");
   }
 
   private function writeEndNode(PhutilCalendarNode $node) {
     $type = $this->getICSNodeType($node);
     return $this->wrapICSLine("END:{$type}");
   }
 
   private function writeNodeProperties(PhutilCalendarNode $node) {
     $properties = $this->getNodeProperties($node);
 
     $out = array();
     foreach ($properties as $property) {
       $propname = $property['name'];
       $propvalue = $property['value'];
 
       $propline = array();
       $propline[] = $propname;
 
       foreach ($property['parameters'] as $parameter) {
         $paramname = $parameter['name'];
         $paramvalue = $parameter['value'];
         $propline[] = ";{$paramname}={$paramvalue}";
       }
 
       $propline[] = ":{$propvalue}";
       $propline = implode('', $propline);
 
       $out[] = $this->wrapICSLine($propline);
     }
 
     return implode('', $out);
   }
 
   private function getICSNodeType(PhutilCalendarNode $node) {
     switch ($node->getNodeType()) {
       case PhutilCalendarDocumentNode::NODETYPE:
         return 'VCALENDAR';
       case PhutilCalendarEventNode::NODETYPE:
         return 'VEVENT';
       default:
         return null;
     }
   }
 
   private function wrapICSLine($line) {
     $out = array();
     $buf = '';
 
     // NOTE: The line may contain sequences of combining characters which are
     // more than 80 bytes in length. If it does, we'll split them in the
     // middle of the sequence. This is okay and generally anticipated by
     // RFC5545, which even allows implementations to split multibyte
     // characters. The sequence will be stitched back together properly by
     // whatever is parsing things.
 
     foreach (phutil_utf8v($line) as $character) {
       // If adding this character would bring the line over 75 bytes, start
       // a new line.
       if (strlen($buf) + strlen($character) > 75) {
         $out[] = $buf."\r\n";
         $buf = ' ';
       }
 
       $buf .= $character;
     }
 
     $out[] = $buf."\r\n";
 
     return implode('', $out);
   }
 
   private function getNodeProperties(PhutilCalendarNode $node) {
     switch ($node->getNodeType()) {
       case PhutilCalendarDocumentNode::NODETYPE:
         return $this->getDocumentNodeProperties($node);
       case PhutilCalendarEventNode::NODETYPE:
         return $this->getEventNodeProperties($node);
       default:
         return array();
     }
   }
 
   private function getDocumentNodeProperties(
     PhutilCalendarDocumentNode $event) {
     $properties = array();
 
     $properties[] = $this->newTextProperty(
       'VERSION',
       '2.0');
 
     $properties[] = $this->newTextProperty(
       'PRODID',
       self::getICSPRODID());
 
     return $properties;
   }
 
   public static function getICSPRODID() {
     return '-//Phacility//Phabricator//EN';
   }
 
   private function getEventNodeProperties(PhutilCalendarEventNode $event) {
     $properties = array();
 
     $uid = $event->getUID();
     if (!strlen($uid)) {
       throw new Exception(
         pht(
           'Unable to write ICS document: event has no UID, but each event '.
           'MUST have a UID.'));
     }
     $properties[] = $this->newTextProperty(
       'UID',
       $uid);
 
     $created = $event->getCreatedDateTime();
     if ($created) {
       $properties[] = $this->newDateTimeProperty(
         'CREATED',
         $event->getCreatedDateTime());
     }
 
     $dtstamp = $event->getModifiedDateTime();
     if (!$dtstamp) {
       throw new Exception(
         pht(
           'Unable to write ICS document: event has no modified time, but '.
           'each event MUST have a modified time.'));
     }
     $properties[] = $this->newDateTimeProperty(
       'DTSTAMP',
       $dtstamp);
 
     $dtstart = $event->getStartDateTime();
     if ($dtstart) {
       $properties[] = $this->newDateTimeProperty(
         'DTSTART',
         $dtstart);
     }
 
     $dtend = $event->getEndDateTime();
     if ($dtend) {
       $properties[] = $this->newDateTimeProperty(
         'DTEND',
         $event->getEndDateTime());
     }
 
     $name = $event->getName();
-    if (strlen($name)) {
+    if (phutil_nonempty_string($name)) {
       $properties[] = $this->newTextProperty(
         'SUMMARY',
         $name);
     }
 
     $description = $event->getDescription();
-    if (strlen($description)) {
+    if (phutil_nonempty_string($description)) {
       $properties[] = $this->newTextProperty(
         'DESCRIPTION',
         $description);
     }
 
     $organizer = $event->getOrganizer();
     if ($organizer) {
       $properties[] = $this->newUserProperty(
         'ORGANIZER',
         $organizer);
     }
 
     $attendees = $event->getAttendees();
     if ($attendees) {
       foreach ($attendees as $attendee) {
         $properties[] = $this->newUserProperty(
           'ATTENDEE',
           $attendee);
       }
     }
 
     $rrule = $event->getRecurrenceRule();
     if ($rrule) {
       $properties[] = $this->newRRULEProperty(
         'RRULE',
         $rrule);
     }
 
     $recurrence_id = $event->getRecurrenceID();
     if ($recurrence_id) {
       $properties[] = $this->newTextProperty(
         'RECURRENCE-ID',
         $recurrence_id);
     }
 
     $exdates = $event->getRecurrenceExceptions();
     if ($exdates) {
       $properties[] = $this->newDateTimesProperty(
         'EXDATE',
         $exdates);
     }
 
     $rdates = $event->getRecurrenceDates();
     if ($rdates) {
       $properties[] = $this->newDateTimesProperty(
         'RDATE',
         $rdates);
     }
 
     return $properties;
   }
 
   private function newTextProperty(
     $name,
     $value,
     array $parameters = array()) {
 
     $map = array(
       '\\' => '\\\\',
       ',' => '\\,',
       "\n" => '\\n',
     );
 
     $value = (array)$value;
     foreach ($value as $k => $v) {
       $v = str_replace(array_keys($map), array_values($map), $v);
       $value[$k] = $v;
     }
 
     $value = implode(',', $value);
 
     return $this->newProperty($name, $value, $parameters);
   }
 
   private function newDateTimeProperty(
     $name,
     PhutilCalendarDateTime $value,
     array $parameters = array()) {
 
     return $this->newDateTimesProperty($name, array($value), $parameters);
   }
 
   private function newDateTimesProperty(
     $name,
     array $values,
     array $parameters = array()) {
     assert_instances_of($values, 'PhutilCalendarDateTime');
 
     if (head($values)->getIsAllDay()) {
       $parameters[] = array(
         'name' => 'VALUE',
         'values' => array(
           'DATE',
         ),
       );
     }
 
     $datetimes = array();
     foreach ($values as $value) {
       $datetimes[] = $value->getISO8601();
     }
     $datetimes = implode(';', $datetimes);
 
     return $this->newProperty($name, $datetimes, $parameters);
   }
 
   private function newUserProperty(
     $name,
     PhutilCalendarUserNode $value,
     array $parameters = array()) {
 
     $parameters[] = array(
       'name' => 'CN',
       'values' => array(
         $value->getName(),
       ),
     );
 
     $partstat = null;
     switch ($value->getStatus()) {
       case PhutilCalendarUserNode::STATUS_INVITED:
         $partstat = 'NEEDS-ACTION';
         break;
       case PhutilCalendarUserNode::STATUS_ACCEPTED:
         $partstat = 'ACCEPTED';
         break;
       case PhutilCalendarUserNode::STATUS_DECLINED:
         $partstat = 'DECLINED';
         break;
     }
 
     if ($partstat !== null) {
       $parameters[] = array(
         'name' => 'PARTSTAT',
         'values' => array(
           $partstat,
         ),
       );
     }
 
     // TODO: We could reasonably fill in "ROLE" and "RSVP" here too, but it
     // isn't clear if these are important to external programs or not.
 
     return $this->newProperty($name, $value->getURI(), $parameters);
   }
 
   private function newRRULEProperty(
     $name,
     PhutilCalendarRecurrenceRule $rule,
     array $parameters = array()) {
 
     $value = $rule->toRRULE();
     return $this->newProperty($name, $value, $parameters);
   }
 
   private function newProperty(
     $name,
     $value,
     array $parameters = array()) {
 
     $map = array(
       '^' => '^^',
       "\n" => '^n',
       '"' => "^'",
     );
 
     $writable_params = array();
     foreach ($parameters as $k => $parameter) {
       $value_list = array();
       foreach ($parameter['values'] as $v) {
         $v = str_replace(array_keys($map), array_values($map), $v);
 
         // If the parameter value isn't a very simple one, quote it.
 
         // RFC5545 says that we MUST quote it if it has a colon, a semicolon,
         // or a comma, and that we MUST quote it if it's a URI.
         if (!preg_match('/^[A-Za-z0-9-]*\z/', $v)) {
           $v = '"'.$v.'"';
         }
 
         $value_list[] = $v;
       }
 
       $writable_params[] = array(
         'name' => $parameter['name'],
         'value' => implode(',', $value_list),
       );
     }
 
     return array(
       'name' => $name,
       'value' => $value,
       'parameters' => $writable_params,
     );
   }
 
 }
diff --git a/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
index b3425ba313..bb850edc85 100644
--- a/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
+++ b/src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
@@ -1,136 +1,140 @@
 <?php
 
 final class PhabricatorFileStorageFormatTestCase extends PhabricatorTestCase {
 
   protected function getPhabricatorTestCaseConfiguration() {
     return array(
       self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
     );
   }
 
   public function testRot13Storage() {
     $engine = new PhabricatorTestStorageEngine();
     $rot13_format = PhabricatorFileROT13StorageFormat::FORMATKEY;
 
     $data = 'The cow jumped over the full moon.';
     $expect = 'Gur pbj whzcrq bire gur shyy zbba.';
 
     $params = array(
       'name' => 'test.dat',
       'storageEngines' => array(
         $engine,
       ),
       'format' => $rot13_format,
     );
 
     $file = PhabricatorFile::newFromFileData($data, $params);
 
     // We should have a file stored as rot13, which reads back the input
     // data correctly.
     $this->assertEqual($rot13_format, $file->getStorageFormat());
     $this->assertEqual($data, $file->loadFileData());
 
     // The actual raw data in the storage engine should be encoded.
     $raw_data = $engine->readFile($file->getStorageHandle());
     $this->assertEqual($expect, $raw_data);
 
     // If we generate an iterator over a slice of the file, it should return
     // the decrypted file.
     $iterator = $file->getFileDataIterator(4, 14);
     $raw_data = '';
     foreach ($iterator as $data_chunk) {
       $raw_data .= $data_chunk;
     }
     $this->assertEqual('cow jumped', $raw_data);
   }
 
   public function testAES256Storage() {
+    if (!function_exists('openssl_encrypt')) {
+      $this->assertSkipped(pht('No OpenSSL extension available.'));
+    }
+
     $engine = new PhabricatorTestStorageEngine();
 
     $key_name = 'test.abcd';
     $key_text = 'abcdefghijklmnopABCDEFGHIJKLMNOP';
 
     PhabricatorKeyring::addKey(
       array(
         'name' => $key_name,
         'type' => 'aes-256-cbc',
         'material.base64' => base64_encode($key_text),
       ));
 
     $format = id(new PhabricatorFileAES256StorageFormat())
       ->selectMasterKey($key_name);
 
     $data = 'The cow jumped over the full moon.';
 
     $params = array(
       'name' => 'test.dat',
       'storageEngines' => array(
         $engine,
       ),
       'format' => $format,
     );
 
     $file = PhabricatorFile::newFromFileData($data, $params);
 
     // We should have a file stored as AES256.
     $format_key = $format->getStorageFormatKey();
     $this->assertEqual($format_key, $file->getStorageFormat());
     $this->assertEqual($data, $file->loadFileData());
 
     // The actual raw data in the storage engine should be encrypted. We
     // can't really test this, but we can make sure it's not the same as the
     // input data.
     $raw_data = $engine->readFile($file->getStorageHandle());
     $this->assertTrue($data !== $raw_data);
 
     // If we generate an iterator over a slice of the file, it should return
     // the decrypted file.
     $iterator = $file->getFileDataIterator(4, 14);
     $raw_data = '';
     foreach ($iterator as $data_chunk) {
       $raw_data .= $data_chunk;
     }
     $this->assertEqual('cow jumped', $raw_data);
 
     $iterator = $file->getFileDataIterator(4, null);
     $raw_data = '';
     foreach ($iterator as $data_chunk) {
       $raw_data .= $data_chunk;
     }
     $this->assertEqual('cow jumped over the full moon.', $raw_data);
 
   }
 
   public function testStorageTampering() {
     $engine = new PhabricatorTestStorageEngine();
 
     $good = 'The cow jumped over the full moon.';
     $evil = 'The cow slept quietly, honoring the glorious dictator.';
 
     $params = array(
       'name' => 'message.txt',
       'storageEngines' => array(
         $engine,
       ),
     );
 
     // First, write the file normally.
     $file = PhabricatorFile::newFromFileData($good, $params);
     $this->assertEqual($good, $file->loadFileData());
 
     // As an adversary, tamper with the file.
     $engine->tamperWithFile($file->getStorageHandle(), $evil);
 
     // Attempts to read the file data should now fail the integrity check.
     $caught = null;
     try {
       $file->loadFileData();
     } catch (PhabricatorFileIntegrityException $ex) {
       $caught = $ex;
     }
 
     $this->assertTrue($caught instanceof PhabricatorFileIntegrityException);
   }
 
 
 }
diff --git a/src/applications/nuance/github/NuanceGitHubRawEvent.php b/src/applications/nuance/github/NuanceGitHubRawEvent.php
index b28a9222dc..8652dca9ad 100644
--- a/src/applications/nuance/github/NuanceGitHubRawEvent.php
+++ b/src/applications/nuance/github/NuanceGitHubRawEvent.php
@@ -1,391 +1,391 @@
 <?php
 
 final class NuanceGitHubRawEvent extends Phobject {
 
   private $raw;
   private $type;
 
   const TYPE_ISSUE = 'issue';
   const TYPE_REPOSITORY = 'repository';
 
   public static function newEvent($type, array $raw) {
     $event = new self();
     $event->type = $type;
     $event->raw = $raw;
     return $event;
   }
 
   public function getRepositoryFullName() {
     return $this->getRepositoryFullRawName();
   }
 
   public function isIssueEvent() {
     if ($this->isPullRequestEvent()) {
       return false;
     }
 
     if ($this->type == self::TYPE_ISSUE) {
       return true;
     }
 
     switch ($this->getIssueRawKind()) {
       case 'IssuesEvent':
         return true;
       case 'IssueCommentEvent':
         if (!$this->getRawPullRequestData()) {
           return true;
         }
         break;
     }
 
     return false;
   }
 
   public function isPullRequestEvent() {
     if ($this->type == self::TYPE_ISSUE) {
       // TODO: This is wrong, some of these are pull events.
       return false;
     }
 
     $raw = $this->raw;
 
     switch ($this->getIssueRawKind()) {
       case 'PullRequestEvent':
         return true;
       case 'IssueCommentEvent':
         if ($this->getRawPullRequestData()) {
           return true;
         }
         break;
     }
 
     return false;
   }
 
   public function getIssueNumber() {
     if (!$this->isIssueEvent()) {
       return null;
     }
 
     return $this->getRawIssueNumber();
   }
 
   public function getPullRequestNumber() {
     if (!$this->isPullRequestEvent()) {
       return null;
     }
 
     return $this->getRawIssueNumber();
   }
 
 
   public function getID() {
     $raw = $this->raw;
 
     $id = idx($raw, 'id');
     if ($id) {
       return (int)$id;
     }
 
     return null;
   }
 
   public function getComment() {
     if (!$this->isIssueEvent() && !$this->isPullRequestEvent()) {
       return null;
     }
 
     $raw = $this->raw;
 
     return idxv($raw, array('payload', 'comment', 'body'));
   }
 
   public function getURI() {
     $raw = $this->raw;
 
     if ($this->isIssueEvent() || $this->isPullRequestEvent()) {
       if ($this->type == self::TYPE_ISSUE) {
         $uri = idxv($raw, array('issue', 'html_url'));
         $uri = $uri.'#event-'.$this->getID();
       } else {
         // The format of pull request events varies so we need to fish around
         // a bit to find the correct URI.
         $uri = idxv($raw, array('payload', 'pull_request', 'html_url'));
         $need_anchor = true;
 
         // For comments, we get a different anchor to link to the comment. In
         // this case, the URI comes with an anchor already.
         if (!$uri) {
           $uri = idxv($raw, array('payload', 'comment', 'html_url'));
           $need_anchor = false;
         }
 
         if (!$uri) {
           $uri = idxv($raw, array('payload', 'issue', 'html_url'));
           $need_anchor = true;
         }
 
         if ($need_anchor) {
           $uri = $uri.'#event-'.$this->getID();
         }
       }
     } else {
       switch ($this->getIssueRawKind()) {
         case 'CreateEvent':
           $ref = idxv($raw, array('payload', 'ref'));
 
           $repo = $this->getRepositoryFullRawName();
           return "https://github.com/{$repo}/commits/{$ref}";
         case 'PushEvent':
           // These don't really have a URI since there may be multiple commits
           // involved and GitHub doesn't bundle the push as an object on its
           // own. Just try to find the URI for the log. The API also does
           // not return any HTML URI for these events.
 
           $head = idxv($raw, array('payload', 'head'));
           if ($head === null) {
             return null;
           }
 
           $repo = $this->getRepositoryFullRawName();
           return "https://github.com/{$repo}/commits/{$head}";
         case 'WatchEvent':
           // These have no reasonable URI.
           return null;
         default:
           return null;
       }
     }
 
     return $uri;
   }
 
   private function getRepositoryFullRawName() {
     $raw = $this->raw;
 
     $full = idxv($raw, array('repo', 'name'));
-    if (strlen($full)) {
+    if (phutil_nonempty_string($full)) {
       return $full;
     }
 
     // For issue events, the repository is not identified explicitly in the
     // response body. Parse it out of the URI.
 
     $matches = null;
     $ok = preg_match(
       '(/repos/((?:[^/]+)/(?:[^/]+))/issues/events/)',
       idx($raw, 'url'),
       $matches);
 
     if ($ok) {
       return $matches[1];
     }
 
     return null;
   }
 
   private function getIssueRawKind() {
     $raw = $this->raw;
     return idxv($raw, array('type'));
   }
 
   private function getRawIssueNumber() {
     $raw = $this->raw;
 
     if ($this->type == self::TYPE_ISSUE) {
       return idxv($raw, array('issue', 'number'));
     }
 
     if ($this->type == self::TYPE_REPOSITORY) {
       $issue_number = idxv($raw, array('payload', 'issue', 'number'));
       if ($issue_number) {
         return $issue_number;
       }
 
       $pull_number = idxv($raw, array('payload', 'number'));
       if ($pull_number) {
         return $pull_number;
       }
     }
 
     return null;
   }
 
   private function getRawPullRequestData() {
     $raw = $this->raw;
     return idxv($raw, array('payload', 'issue', 'pull_request'));
   }
 
   public function getEventFullTitle() {
     switch ($this->type) {
       case self::TYPE_ISSUE:
         $title = $this->getRawIssueEventTitle();
         break;
       case self::TYPE_REPOSITORY:
         $title = $this->getRawRepositoryEventTitle();
         break;
       default:
         $title = pht('Unknown Event Type ("%s")', $this->type);
         break;
     }
 
     return pht(
       'GitHub %s %s (%s)',
       $this->getRepositoryFullRawName(),
       $this->getTargetObjectName(),
       $title);
   }
 
   public function getActorGitHubUserID() {
     $raw = $this->raw;
     return (int)idxv($raw, array('actor', 'id'));
   }
 
   private function getTargetObjectName() {
     if ($this->isPullRequestEvent()) {
       $number = $this->getRawIssueNumber();
       return pht('Pull Request #%d', $number);
     } else if ($this->isIssueEvent()) {
       $number = $this->getRawIssueNumber();
       return pht('Issue #%d', $number);
     } else if ($this->type == self::TYPE_REPOSITORY) {
       $raw = $this->raw;
 
 
       $type = idx($raw, 'type');
       switch ($type) {
         case 'CreateEvent':
           $ref = idxv($raw, array('payload', 'ref'));
           $ref_type = idxv($raw, array('payload', 'ref_type'));
 
           switch ($ref_type) {
             case 'branch':
               return pht('Branch %s', $ref);
             case 'tag':
               return pht('Tag %s', $ref);
             default:
               return pht('Ref %s', $ref);
           }
           break;
         case 'PushEvent':
           $ref = idxv($raw, array('payload', 'ref'));
           if (preg_match('(^refs/heads/)', $ref)) {
             return pht('Branch %s', substr($ref, strlen('refs/heads/')));
           } else {
             return pht('Ref %s', $ref);
           }
           break;
         case 'WatchEvent':
           $actor = idxv($raw, array('actor', 'login'));
           return pht('User %s', $actor);
       }
 
       return pht('Unknown Object');
     } else {
       return pht('Unknown Object');
     }
   }
 
   private function getRawIssueEventTitle() {
     $raw = $this->raw;
 
     $action = idxv($raw, array('event'));
     switch ($action) {
       case 'assigned':
         $assignee = idxv($raw, array('assignee', 'login'));
         $title = pht('Assigned: %s', $assignee);
         break;
       case 'closed':
         $title = pht('Closed');
         break;
       case 'demilestoned':
         $milestone = idxv($raw, array('milestone', 'title'));
         $title = pht('Removed Milestone: %s', $milestone);
         break;
       case 'labeled':
         $label = idxv($raw, array('label', 'name'));
         $title = pht('Added Label: %s', $label);
         break;
       case 'locked':
         $title = pht('Locked');
         break;
       case 'milestoned':
         $milestone = idxv($raw, array('milestone', 'title'));
         $title = pht('Added Milestone: %s', $milestone);
         break;
       case 'renamed':
         $title = pht('Renamed');
         break;
       case 'reopened':
         $title = pht('Reopened');
         break;
       case 'unassigned':
         $assignee = idxv($raw, array('assignee', 'login'));
         $title = pht('Unassigned: %s', $assignee);
         break;
       case 'unlabeled':
         $label = idxv($raw, array('label', 'name'));
         $title = pht('Removed Label: %s', $label);
         break;
       case 'unlocked':
         $title = pht('Unlocked');
         break;
       default:
         $title = pht('"%s"', $action);
         break;
     }
 
 
     return $title;
   }
 
   private function getRawRepositoryEventTitle() {
     $raw = $this->raw;
 
     $type = idx($raw, 'type');
     switch ($type) {
       case 'CreateEvent':
         return pht('Created');
       case 'PushEvent':
         $head = idxv($raw, array('payload', 'head'));
         $head = substr($head, 0, 12);
         return pht('Pushed: %s', $head);
       case 'IssuesEvent':
         $action = idxv($raw, array('payload', 'action'));
         switch ($action) {
           case 'closed':
             return pht('Closed');
           case 'opened':
             return pht('Created');
           case 'reopened':
             return pht('Reopened');
           default:
             return pht('"%s"', $action);
         }
         break;
       case 'IssueCommentEvent':
         $action = idxv($raw, array('payload', 'action'));
         switch ($action) {
           case 'created':
             return pht('Comment');
           default:
             return pht('"%s"', $action);
         }
         break;
       case 'PullRequestEvent':
         $action = idxv($raw, array('payload', 'action'));
         switch ($action) {
           case 'opened':
             return pht('Created');
           default:
             return pht('"%s"', $action);
         }
         break;
       case 'WatchEvent':
         return pht('Watched');
     }
 
     return pht('"%s"', $type);
   }
 
 }
diff --git a/src/infrastructure/markup/blockrule/PhutilRemarkupListBlockRule.php b/src/infrastructure/markup/blockrule/PhutilRemarkupListBlockRule.php
index 5bdcab2b8f..b7d82edfef 100644
--- a/src/infrastructure/markup/blockrule/PhutilRemarkupListBlockRule.php
+++ b/src/infrastructure/markup/blockrule/PhutilRemarkupListBlockRule.php
@@ -1,567 +1,567 @@
 <?php
 
 final class PhutilRemarkupListBlockRule extends PhutilRemarkupBlockRule {
 
   /**
    * This rule must apply before the Code block rule because it needs to
    * win blocks which begin `  - Lorem ipsum`.
    */
   public function getPriority() {
     return 400;
   }
 
   public function getMatchingLineCount(array $lines, $cursor) {
     $num_lines = 0;
 
     $first_line = $cursor;
     $is_one_line = false;
     while (isset($lines[$cursor])) {
       if (!$num_lines) {
         if (preg_match(self::START_BLOCK_PATTERN, $lines[$cursor])) {
           $num_lines++;
           $cursor++;
           $is_one_line = true;
           continue;
         }
       } else {
         if (preg_match(self::CONT_BLOCK_PATTERN, $lines[$cursor])) {
           $num_lines++;
           $cursor++;
           $is_one_line = false;
           continue;
         }
 
         // Allow lists to continue across multiple paragraphs, as long as lines
         // are indented or a single empty line separates indented lines.
 
         $this_empty = !strlen(trim($lines[$cursor]));
         $this_indented = preg_match('/^ /', $lines[$cursor]);
 
         $next_empty = true;
         $next_indented = false;
         if (isset($lines[$cursor + 1])) {
           $next_empty = !strlen(trim($lines[$cursor + 1]));
           $next_indented = preg_match('/^ /', $lines[$cursor + 1]);
         }
 
         if ($this_empty || $this_indented) {
           if (($this_indented && !$this_empty) ||
               ($next_indented && !$next_empty)) {
             $num_lines++;
             $cursor++;
             continue;
           }
         }
 
         if ($this_empty) {
           $num_lines++;
         }
       }
 
       break;
     }
 
     // If this list only has one item in it, and the list marker is "#", and
     // it's not the last line in the input, parse it as a header instead of a
     // list. This produces better behavior for alternate Markdown headers.
 
     if ($is_one_line) {
       if (($first_line + $num_lines) < count($lines)) {
         if (strncmp($lines[$first_line], '#', 1) === 0) {
           return 0;
         }
       }
     }
 
     return $num_lines;
   }
 
   /**
    * The maximum sub-list depth you can nest to. Avoids silliness and blowing
    * the stack.
    */
   const MAXIMUM_LIST_NESTING_DEPTH = 12;
   const START_BLOCK_PATTERN = '@^\s*(?:[-*#]+|([1-9][0-9]*)[.)]|\[\D?\])\s+@';
   const CONT_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)]|\[\D?\])\s+@';
   const STRIP_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)])\s*@';
 
   public function markupText($text, $children) {
     $items = array();
     $lines = explode("\n", $text);
 
     // We allow users to delimit lists using either differing indentation
     // levels:
     //
     //   - a
     //     - b
     //
     // ...or differing numbers of item-delimiter characters:
     //
     //   - a
     //   -- b
     //
     // If they use the second style but block-indent the whole list, we'll
     // get the depth counts wrong for the first item. To prevent this,
     // un-indent every item by the minimum indentation level for the whole
     // block before we begin parsing.
 
     $regex = self::START_BLOCK_PATTERN;
     $min_space = PHP_INT_MAX;
     foreach ($lines as $ii => $line) {
       $matches = null;
       if (preg_match($regex, $line)) {
         $regex = self::CONT_BLOCK_PATTERN;
         if (preg_match('/^(\s+)/', $line, $matches)) {
           $space = strlen($matches[1]);
         } else {
           $space = 0;
         }
         $min_space = min($min_space, $space);
       }
     }
 
     $regex = self::START_BLOCK_PATTERN;
     if ($min_space) {
       foreach ($lines as $key => $line) {
         if (preg_match($regex, $line)) {
           $regex = self::CONT_BLOCK_PATTERN;
           $lines[$key] = substr($line, $min_space);
         }
       }
     }
 
 
     // The input text may have linewraps in it, like this:
     //
     //   - derp derp derp derp
     //     derp derp derp derp
     //   - blarp blarp blarp blarp
     //
     // Group text lines together into list items, stored in $items. So the
     // result in the above case will be:
     //
     //   array(
     //     array(
     //       "- derp derp derp derp",
     //       "  derp derp derp derp",
     //     ),
     //     array(
     //       "- blarp blarp blarp blarp",
     //     ),
     //   );
 
     $item = array();
     $starts_at = null;
     $regex = self::START_BLOCK_PATTERN;
     foreach ($lines as $line) {
       $match = null;
       if (preg_match($regex, $line, $match)) {
         if (!$starts_at && !empty($match[1])) {
           $starts_at = $match[1];
         }
         $regex = self::CONT_BLOCK_PATTERN;
         if ($item) {
           $items[] = $item;
           $item = array();
         }
       }
       $item[] = $line;
     }
     if ($item) {
       $items[] = $item;
     }
     if (!$starts_at) {
       $starts_at = 1;
     }
 
 
     // Process each item to normalize the text, remove line wrapping, and
     // determine its depth (indentation level) and style (ordered vs unordered).
     //
     // We preserve consecutive linebreaks and interpret them as paragraph
     // breaks.
     //
     // Given the above example, the processed array will look like:
     //
     //   array(
     //     array(
     //       'text'  => 'derp derp derp derp derp derp derp derp',
     //       'depth' => 0,
     //       'style' => '-',
     //     ),
     //     array(
     //       'text'  => 'blarp blarp blarp blarp',
     //       'depth' => 0,
     //       'style' => '-',
     //     ),
     //   );
 
     $has_marks = false;
     foreach ($items as $key => $item) {
       // Trim space around newlines, to strip trailing whitespace and formatting
       // indentation.
       $item = preg_replace('/ *(\n+) */', '\1', implode("\n", $item));
 
       // Replace single newlines with a space. Preserve multiple newlines as
       // paragraph breaks.
       $item = preg_replace('/(?<!\n)\n(?!\n)/', ' ', $item);
 
       $item = rtrim($item);
 
       if (!strlen($item)) {
         unset($items[$key]);
         continue;
       }
 
       $matches = null;
       if (preg_match('/^\s*([-*#]{2,})/', $item, $matches)) {
         // Alternate-style indents; use number of list item symbols.
         $depth = strlen($matches[1]) - 1;
       } else if (preg_match('/^(\s+)/', $item, $matches)) {
         // Markdown-style indents; use indent depth.
         $depth = strlen($matches[1]);
       } else {
         $depth = 0;
       }
 
       if (preg_match('/^\s*(?:#|[0-9])/', $item)) {
         $style = '#';
       } else {
         $style = '-';
       }
 
       // Strip leading indicators off the item.
       $text = preg_replace(self::STRIP_BLOCK_PATTERN, '', $item);
 
       // Look for "[]", "[ ]", "[*]", "[x]", etc., which we render as a
       // checkbox. We don't render [1], [2], etc., as checkboxes, as these
       // are often used as footnotes.
       $mark = null;
       $matches = null;
       if (preg_match('/^\s*\[(\D?)\]\s*/', $text, $matches)) {
         if (strlen(trim($matches[1]))) {
           $mark = true;
         } else {
           $mark = false;
         }
         $has_marks = true;
         $text = substr($text, strlen($matches[0]));
       }
 
       $items[$key] = array(
         'text'  => $text,
         'depth' => $depth,
         'style' => $style,
         'mark'  => $mark,
       );
     }
     $items = array_values($items);
 
 
     // Users can create a sub-list by indenting any deeper amount than the
     // previous list, so these are both valid:
     //
     //   - a
     //     - b
     //
     //   - a
     //       - b
     //
     // In the former case, we'll have depths (0, 2). In the latter case, depths
     // (0, 4). We don't actually care about how many spaces there are, only
     // how many list indentation levels (that is, we want to map both of
     // those cases to (0, 1), indicating "outermost list" and "first sublist").
     //
     // This is made more complicated because lists at two different indentation
     // levels might be at the same list level:
     //
     //   - a
     //     - b
     //   - c
     //       - d
     //
     // Here, 'b' and 'd' are at the same list level (2) but different indent
     // levels (2, 4).
     //
     // Users can also create "staircases" like this:
     //
     //       - a
     //     - b
     //   # c
     //
     // While this is silly, we'd like to render it as faithfully as possible.
     //
     // In order to do this, we convert the list of nodes into a tree,
     // normalizing indentation levels and inserting dummy nodes as necessary to
     // make the tree well-formed. See additional notes at buildTree().
     //
     // In the case above, the result is a tree like this:
     //
     //   - <null>
     //     - <null>
     //       - a
     //     - b
     //   # c
 
     $l = 0;
     $r = count($items);
     $tree = $this->buildTree($items, $l, $r, $cur_level = 0);
 
 
     // We may need to open a list on a <null> node, but they do not have
     // list style information yet. We need to propagate list style information
     // backward through the tree. In the above example, the tree now looks
     // like this:
     //
     //   - <null (style=#)>
     //     - <null (style=-)>
     //       - a
     //     - b
     //   # c
 
     $this->adjustTreeStyleInformation($tree);
 
     // Finally, we have enough information to render the tree.
 
     $out = $this->renderTree($tree, 0, $has_marks, $starts_at);
 
     if ($this->getEngine()->isTextMode()) {
       $out = implode('', $out);
       $out = rtrim($out, "\n");
       $out = preg_replace('/ +$/m', '', $out);
       return $out;
     }
 
     return phutil_implode_html('', $out);
   }
 
   /**
    * See additional notes in @{method:markupText}.
    */
   private function buildTree(array $items, $l, $r, $cur_level) {
     if ($l == $r) {
       return array();
     }
 
     if ($cur_level > self::MAXIMUM_LIST_NESTING_DEPTH) {
       // This algorithm is recursive and we don't need you blowing the stack
       // with your oh-so-clever 50,000-item-deep list. Cap indentation levels
       // at a reasonable number and just shove everything deeper up to this
       // level.
       $nodes = array();
       for ($ii = $l; $ii < $r; $ii++) {
         $nodes[] = array(
           'level' => $cur_level,
           'items' => array(),
         ) + $items[$ii];
       }
       return $nodes;
     }
 
     $min = $l;
     for ($ii = $r - 1; $ii >= $l; $ii--) {
       if ($items[$ii]['depth'] <= $items[$min]['depth']) {
         $min = $ii;
       }
     }
 
     $min_depth = $items[$min]['depth'];
 
     $nodes = array();
     if ($min != $l) {
       $nodes[] = array(
         'text'    => null,
         'level'   => $cur_level,
         'style'   => null,
         'mark'    => null,
         'items'   => $this->buildTree($items, $l, $min, $cur_level + 1),
       );
     }
 
     $last = $min;
     for ($ii = $last + 1; $ii < $r; $ii++) {
       if ($items[$ii]['depth'] == $min_depth) {
         $nodes[] = array(
           'level' => $cur_level,
           'items' => $this->buildTree($items, $last + 1, $ii, $cur_level + 1),
         ) + $items[$last];
         $last = $ii;
       }
     }
     $nodes[] = array(
       'level' => $cur_level,
       'items' => $this->buildTree($items, $last + 1, $r, $cur_level + 1),
     ) + $items[$last];
 
     return $nodes;
   }
 
 
   /**
    * See additional notes in @{method:markupText}.
    */
   private function adjustTreeStyleInformation(array &$tree) {
     // The effect here is just to walk backward through the nodes at this level
     // and apply the first style in the list to any empty nodes we inserted
     // before it. As we go, also recurse down the tree.
 
     $style = '-';
     for ($ii = count($tree) - 1; $ii >= 0; $ii--) {
       if ($tree[$ii]['style'] !== null) {
         // This is the earliest node we've seen with style, so set the
         // style to its style.
         $style = $tree[$ii]['style'];
       } else {
         // This node has no style, so apply the current style.
         $tree[$ii]['style'] = $style;
       }
       if ($tree[$ii]['items']) {
         $this->adjustTreeStyleInformation($tree[$ii]['items']);
       }
     }
   }
 
 
   /**
    * See additional notes in @{method:markupText}.
    */
   private function renderTree(
     array $tree,
     $level,
     $has_marks,
     $starts_at = 1) {
 
     $style = idx(head($tree), 'style');
 
     $out = array();
 
     if (!$this->getEngine()->isTextMode()) {
       switch ($style) {
         case '#':
           $tag = 'ol';
           break;
         case '-':
           $tag = 'ul';
           break;
       }
 
       $start_attr = null;
-      if (ctype_digit($starts_at) && $starts_at > 1) {
+      if (ctype_digit(phutil_string_cast($starts_at)) && $starts_at > 1) {
         $start_attr = hsprintf(' start="%d"', $starts_at);
       }
 
       if ($has_marks) {
         $out[] = hsprintf(
           '<%s class="remarkup-list remarkup-list-with-checkmarks"%s>',
           $tag,
           $start_attr);
       } else {
         $out[] = hsprintf(
           '<%s class="remarkup-list"%s>',
           $tag,
           $start_attr);
       }
 
       $out[] = "\n";
     }
 
     $number = $starts_at;
     foreach ($tree as $item) {
       if ($this->getEngine()->isTextMode()) {
         if ($item['text'] === null) {
           // Don't render anything.
         } else {
           $indent = str_repeat(' ', 2 * $level);
           $out[] = $indent;
           if ($item['mark'] !== null) {
             if ($item['mark']) {
               $out[] = '[X] ';
             } else {
               $out[] = '[ ] ';
             }
           } else {
             switch ($style) {
               case '#':
                 $out[] = $number.'. ';
                 $number++;
                 break;
               case '-':
                 $out[] = '- ';
                 break;
             }
           }
 
           $parts = preg_split('/\n{2,}/', $item['text']);
           foreach ($parts as $key => $part) {
             if ($key != 0) {
               $out[] = "\n\n  ".$indent;
             }
             $out[] = $this->applyRules($part);
           }
           $out[] = "\n";
         }
       } else {
         if ($item['text'] === null) {
           $out[] = hsprintf('<li class="remarkup-list-item phantom-item">');
         } else {
           if ($item['mark'] !== null) {
             if ($item['mark'] == true) {
               $out[] = hsprintf(
                 '<li class="remarkup-list-item remarkup-checked-item">');
             } else {
               $out[] = hsprintf(
                 '<li class="remarkup-list-item remarkup-unchecked-item">');
             }
             $out[] = phutil_tag(
               'input',
               array(
                 'type' => 'checkbox',
                 'checked' => ($item['mark'] ? 'checked' : null),
                 'disabled' => 'disabled',
               ));
             $out[] = ' ';
           } else {
             $out[] = hsprintf('<li class="remarkup-list-item">');
           }
 
           $parts = preg_split('/\n{2,}/', $item['text']);
           foreach ($parts as $key => $part) {
             if ($key != 0) {
               $out[] = array(
                 "\n",
                 phutil_tag('br'),
                 phutil_tag('br'),
                 "\n",
               );
             }
             $out[] = $this->applyRules($part);
           }
         }
       }
 
       if ($item['items']) {
         $subitems = $this->renderTree($item['items'], $level + 1, $has_marks);
         foreach ($subitems as $i) {
           $out[] = $i;
         }
       }
       if (!$this->getEngine()->isTextMode()) {
         $out[] = hsprintf("</li>\n");
       }
     }
 
     if (!$this->getEngine()->isTextMode()) {
       switch ($style) {
         case '#':
           $out[] = hsprintf('</ol>');
           break;
         case '-':
           $out[] = hsprintf('</ul>');
           break;
       }
     }
 
     return $out;
   }
 
 }
diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
index 2170d9ae5e..ded57d4c77 100644
--- a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
+++ b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php
@@ -1,183 +1,183 @@
 <?php
 
 final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule {
 
   public function getPriority() {
     return 150.0;
   }
 
   public function apply($text) {
     // Handle mediawiki-style links: [[ href | name ]]
     $text = preg_replace_callback(
       '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
       array($this, 'markupDocumentLink'),
       $text);
 
     // Handle markdown-style links: [name](href)
     $text = preg_replace_callback(
       '@'.
         '\B'.
         '\\[([^\\]]+)\\]'.
         '\\('.
           '(\s*'.
             // See T12343. This is making some kind of effort to implement
             // parenthesis balancing rules. It won't get nested parentheses
             // right, but should do OK for Wikipedia pages, which seem to be
             // the most important use case.
 
             // Match zero or more non-parenthesis, non-space characters.
             '[^\s()]*'.
             // Match zero or more sequences of "(...)", where two balanced
             // parentheses enclose zero or more normal characters. If we
             // match some, optionally match more stuff at the end.
             '(?:(?:\\([^ ()]*\\))+[^\s()]*)?'.
           '\s*)'.
         '\\)'.
       '\B'.
       '@U',
       array($this, 'markupAlternateLink'),
       $text);
 
     return $text;
   }
 
   protected function renderHyperlink($link, $name) {
     $engine = $this->getEngine();
 
     $is_anchor = false;
     if (strncmp($link, '/', 1) == 0) {
-      $base = $engine->getConfig('uri.base');
+      $base = phutil_string_cast($engine->getConfig('uri.base'));
       $base = rtrim($base, '/');
       $link = $base.$link;
     } else if (strncmp($link, '#', 1) == 0) {
       $here = $engine->getConfig('uri.here');
       $link = $here.$link;
 
       $is_anchor = true;
     }
 
     if ($engine->isTextMode()) {
       // If present, strip off "mailto:" or "tel:".
       $link = preg_replace('/^(?:mailto|tel):/', '', $link);
 
       if (!strlen($name)) {
         return $link;
       }
 
       return $name.' <'.$link.'>';
     }
 
     if (!strlen($name)) {
       $name = $link;
       $name = preg_replace('/^(?:mailto|tel):/', '', $name);
     }
 
     if ($engine->getState('toc')) {
       return $name;
     }
 
     $same_window = $engine->getConfig('uri.same-window', false);
     if ($same_window) {
       $target = null;
     } else {
       $target = '_blank';
     }
 
     // For anchors on the same page, always stay here.
     if ($is_anchor) {
       $target = null;
     }
 
     return phutil_tag(
       'a',
       array(
         'href' => $link,
         'class' => 'remarkup-link',
         'target' => $target,
         'rel' => 'noreferrer',
       ),
       $name);
   }
 
   public function markupAlternateLink(array $matches) {
     $uri = trim($matches[2]);
 
     if (!strlen($uri)) {
       return $matches[0];
     }
 
     // NOTE: We apply some special rules to avoid false positives here. The
     // major concern is that we do not want to convert `x[0][1](y)` in a
     // discussion about C source code into a link. To this end, we:
     //
     //   - Don't match at word boundaries;
     //   - require the URI to contain a "/" character or "@" character; and
     //   - reject URIs which being with a quote character.
 
     if ($uri[0] == '"' || $uri[0] == "'" || $uri[0] == '`') {
       return $matches[0];
     }
 
     if (strpos($uri, '/') === false &&
         strpos($uri, '@') === false &&
         strncmp($uri, 'tel:', 4)) {
       return $matches[0];
     }
 
     return $this->markupDocumentLink(
       array(
         $matches[0],
         $matches[2],
         $matches[1],
       ));
   }
 
   public function markupDocumentLink(array $matches) {
     $uri = trim($matches[1]);
-    $name = trim(idx($matches, 2));
+    $name = trim(idx($matches, 2, ''));
 
     if (!$this->isFlatText($uri)) {
       return $matches[0];
     }
 
     if (!$this->isFlatText($name)) {
       return $matches[0];
     }
 
     // If whatever is being linked to begins with "/" or "#", or has "://",
     // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page.
     $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri);
 
     if ($is_uri && strncmp('/', $uri, 1) && strncmp('#', $uri, 1)) {
       $protocols = $this->getEngine()->getConfig(
         'uri.allowed-protocols',
         array());
 
       try {
         $protocol = id(new PhutilURI($uri))->getProtocol();
         if (!idx($protocols, $protocol)) {
           // Don't treat this as a URI if it's not an allowed protocol.
           $is_uri = false;
         }
       } catch (Exception $ex) {
         // We can end up here if we try to parse an ambiguous URI, see
         // T12796.
         $is_uri = false;
       }
     }
 
     // As a special case, skip "[[ / ]]" so that Phriction picks it up as a
     // link to the Phriction root. It is more useful to be able to use this
     // syntax to link to the root document than the home page of the install.
     if ($uri == '/') {
       $is_uri = false;
     }
 
     if (!$is_uri) {
       return $matches[0];
     }
 
     return $this->getEngine()->storeText($this->renderHyperlink($uri, $name));
   }
 
 }