Page MenuHomestyx hydra

No OneTemporary

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));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Sep 7, 6:46 AM (10 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
222434
Default Alt Text
(47 KB)

Event Timeline