diff --git a/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
index 11f7b5cf1..a246aa1a3 100644
--- a/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
@@ -1,99 +1,99 @@
value;
}
public function setValueFromStorage($value) {
$this->value = $value;
return $this;
}
public function shouldAppearOnEdit() {
return true;
}
public function setValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getStorageKey());
return $this;
}
public function renderEditControl() {
return id(new AphrontFormTextControl())
->setLabel(pht('Blame Revision'))
->setCaption(
pht('Revision which broke the stuff which this change fixes.'))
->setName($this->getStorageKey())
->setValue($this->value);
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return pht('Blame Revision:');
}
public function renderValueForRevisionView() {
if (!$this->value) {
return null;
}
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
- return phutil_safe_html($engine->markupText($this->value));
+ return $engine->markupText($this->value);
}
public function shouldAppearOnConduitView() {
return true;
}
public function getValueForConduit() {
return $this->value;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'blameRevision';
}
public function setValueFromParsedCommitMessage($value) {
$this->value = $value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Blame Revision';
}
public function renderValueForCommitMessage($is_edit) {
return $this->value;
}
public function getSupportedCommitMessageLabels() {
return array(
'Blame Revision',
'Blame Rev',
);
}
public function parseValueFromCommitMessage($value) {
return $value;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
index ae189573f..6b91e90f4 100644
--- a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
@@ -1,223 +1,223 @@
renderLabelForRevisionView();
}
public function renderValueForDiffView() {
return $this->renderValueForRevisionView();
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Unit:';
}
private function getUnitExcuse() {
return $this->getDiffProperty('arc:unit-excuse');
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff);
$umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff);
$rows = array();
$rows[] = array(
'style' => 'star',
'name' => $ustar,
'value' => $umsg,
'show' => true,
);
$excuse = $this->getUnitExcuse();
if ($excuse) {
$rows[] = array(
'style' => 'excuse',
'name' => 'Excuse',
'value' => phutil_escape_html_newlines($excuse),
'show' => true,
);
}
$show_limit = 10;
$hidden = array();
$udata = $this->getDiffProperty('arc:unit');
if ($udata) {
$sort_map = array(
ArcanistUnitTestResult::RESULT_BROKEN => 0,
ArcanistUnitTestResult::RESULT_FAIL => 1,
ArcanistUnitTestResult::RESULT_UNSOUND => 2,
ArcanistUnitTestResult::RESULT_SKIP => 3,
ArcanistUnitTestResult::RESULT_POSTPONED => 4,
ArcanistUnitTestResult::RESULT_PASS => 5,
);
foreach ($udata as $key => $test) {
$udata[$key]['sort'] = idx($sort_map, idx($test, 'result'));
}
$udata = isort($udata, 'sort');
foreach ($udata as $test) {
$result = idx($test, 'result');
$default_hide = false;
switch ($result) {
case ArcanistUnitTestResult::RESULT_POSTPONED:
case ArcanistUnitTestResult::RESULT_PASS:
$default_hide = true;
break;
}
if ($show_limit && !$default_hide) {
--$show_limit;
$show = true;
} else {
$show = false;
if (empty($hidden[$result])) {
$hidden[$result] = 0;
}
$hidden[$result]++;
}
$value = idx($test, 'name');
if (!empty($test['link'])) {
$value = phutil_tag(
'a',
array(
'href' => $test['link'],
'target' => '_blank',
),
$value);
}
$rows[] = array(
'style' => $this->getResultStyle($result),
'name' => ucwords($result),
'value' => $value,
'show' => $show,
);
$userdata = idx($test, 'userdata');
if ($userdata) {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
- $userdata = phutil_safe_html($engine->markupText($userdata));
+ $userdata = $engine->markupText($userdata);
$rows[] = array(
'style' => 'details',
'value' => $userdata,
'show' => false,
);
if (empty($hidden['details'])) {
$hidden['details'] = 0;
}
$hidden['details']++;
}
}
}
$show_string = $this->renderShowString($hidden);
$view = new DifferentialResultsTableView();
$view->setRows($rows);
$view->setShowMoreString($show_string);
return $view->render();
}
private function getResultStyle($result) {
$map = array(
ArcanistUnitTestResult::RESULT_PASS => 'green',
ArcanistUnitTestResult::RESULT_FAIL => 'red',
ArcanistUnitTestResult::RESULT_SKIP => 'blue',
ArcanistUnitTestResult::RESULT_BROKEN => 'red',
ArcanistUnitTestResult::RESULT_UNSOUND => 'yellow',
ArcanistUnitTestResult::RESULT_POSTPONED => 'blue',
);
return idx($map, $result);
}
private function renderShowString(array $hidden) {
if (!$hidden) {
return null;
}
// Reorder hidden things by severity.
$hidden = array_select_keys(
$hidden,
array(
ArcanistUnitTestResult::RESULT_BROKEN,
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_UNSOUND,
ArcanistUnitTestResult::RESULT_SKIP,
ArcanistUnitTestResult::RESULT_POSTPONED,
ArcanistUnitTestResult::RESULT_PASS,
'details',
)) + $hidden;
$noun = array(
ArcanistUnitTestResult::RESULT_BROKEN => 'Broken',
ArcanistUnitTestResult::RESULT_FAIL => 'Failed',
ArcanistUnitTestResult::RESULT_UNSOUND => 'Unsound',
ArcanistUnitTestResult::RESULT_SKIP => 'Skipped',
ArcanistUnitTestResult::RESULT_POSTPONED => 'Postponed',
ArcanistUnitTestResult::RESULT_PASS => 'Passed',
);
$show = array();
foreach ($hidden as $key => $value) {
if ($key == 'details') {
$show[] = pht('%d Detail(s)', $value);
} else {
$show[] = $value.' '.idx($noun, $key);
}
}
return "Show Full Unit Results (".implode(', ', $show).")";
}
public function renderWarningBoxForRevisionAccept() {
$diff = $this->getDiff();
$unit_warning = null;
if ($diff->getUnitStatus() >= DifferentialUnitStatus::UNIT_WARN) {
$titles =
array(
DifferentialUnitStatus::UNIT_WARN => 'Unit Tests Warning',
DifferentialUnitStatus::UNIT_FAIL => 'Unit Tests Failure',
DifferentialUnitStatus::UNIT_SKIP => 'Unit Tests Skipped',
DifferentialUnitStatus::UNIT_POSTPONED => 'Unit Tests Postponed'
);
if ($diff->getUnitStatus() == DifferentialUnitStatus::UNIT_POSTPONED) {
$content =
"This diff has postponed unit tests. The results should be ".
"coming in soon. You should probably wait for them before accepting ".
"this diff.";
} else if ($diff->getUnitStatus() == DifferentialUnitStatus::UNIT_SKIP) {
$content =
"Unit tests were skipped when this diff was created. Make sure ".
"you are OK with that before you accept this diff.";
} else {
$content =
"This diff has Unit Test Problems. Make sure you are OK with ".
"them before you accept this diff.";
}
$unit_warning = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_ERROR)
->appendChild(phutil_tag('p', array(), $content))
->setTitle(idx($titles, $diff->getUnitStatus(), 'Warning'));
}
return $unit_warning;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
index d24b9a209..fbc56ffea 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -1,121 +1,121 @@
diffusionRequest;
if ($this->getRequest()->getStr('before')) {
$results = array();
$is_file = true;
} else {
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$results = $browse_query->loadPaths();
$reason = $browse_query->getReasonForEmptyResultSet();
$is_file = ($reason == DiffusionBrowseQuery::REASON_IS_FILE);
}
$content = array();
if ($drequest->getTagContent()) {
$title = 'Tag: '.$drequest->getSymbolicCommit();
$tag_view = new AphrontPanelView();
$tag_view->setHeader($title);
$tag_view->appendChild(
$this->markupText($drequest->getTagContent()));
$content[] = $tag_view;
}
if (!$results) {
if ($is_file) {
$controller = new DiffusionBrowseFileController($this->getRequest());
$controller->setDiffusionRequest($drequest);
$controller->setCurrentApplication($this->getCurrentApplication());
return $this->delegateToController($controller);
}
$empty_result = new DiffusionEmptyResultView();
$empty_result->setDiffusionRequest($drequest);
$empty_result->setBrowseQuery($browse_query);
$empty_result->setView($this->getRequest()->getStr('view'));
$content[] = $empty_result;
} else {
$phids = array();
foreach ($results as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setHandles($handles);
$browse_table->setPaths($results);
$browse_table->setUser($this->getRequest()->getUser());
$browse_panel = new AphrontPanelView();
$browse_panel->appendChild($browse_table);
$browse_panel->setNoBackground();
$content[] = $browse_panel;
}
$content[] = $this->buildOpenRevisions();
$readme_content = $browse_query->renderReadme($results);
if ($readme_content) {
$readme_panel = new AphrontPanelView();
$readme_panel->setHeader('README');
$readme_panel->appendChild($readme_content);
$content[] = $readme_panel;
}
$nav = $this->buildSideNav('browse', false);
$nav->appendChild($content);
$crumbs = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$nav->setCrumbs($crumbs);
return $this->buildApplicationPage(
$nav,
array(
'title' => array(
nonempty(basename($drequest->getPath()), '/'),
$drequest->getRepository()->getCallsign().' Repository',
),
));
}
private function markupText($text) {
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
- $text = phutil_safe_html($engine->markupText($text));
+ $text = $engine->markupText($text);
$text = phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$text);
return $text;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 3f8720145..db7f75113 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,952 +1,951 @@
diffusionRequest = $drequest;
}
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$user = $request->getUser();
if ($request->getStr('diff')) {
return $this->buildRawDiffResponse($drequest);
}
$callsign = $drequest->getRepository()->getCallsign();
$content = array();
$repository = $drequest->getRepository();
$commit = $drequest->loadCommit();
if (!$commit) {
$query = DiffusionExistsQuery::newFromDiffusionRequest($drequest);
$exists = $query->loadExistentialData();
if (!$exists) {
return new Aphront404Response();
}
return $this->buildStandardPageResponse(
id(new AphrontErrorView())
->setTitle('Error displaying commit.')
->appendChild('Failed to load the commit because the commit has not '.
'been parsed yet.'),
array('title' => 'Commit Still Parsing')
);
}
$commit_data = $drequest->loadCommitData();
$commit->attachCommitData($commit_data);
$top_anchor = id(new PhabricatorAnchorView())
->setAnchorName('top')
->setNavigationMarker(true);
$is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
$changesets = null;
if ($is_foreign) {
$subpath = $commit_data->getCommitDetail('svn-subpath');
$error_panel = new AphrontErrorView();
$error_panel->setTitle('Commit Not Tracked');
$error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$error_panel->appendChild(
"This Diffusion repository is configured to track only one ".
"subdirectory of the entire Subversion repository, and this commit ".
"didn't affect the tracked subdirectory ('".$subpath."'), so no ".
"information is available.");
$content[] = $error_panel;
$content[] = $top_anchor;
} else {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
require_celerity_resource('diffusion-commit-view-css');
require_celerity_resource('phabricator-remarkup-css');
$parent_query = DiffusionCommitParentsQuery::newFromDiffusionRequest(
$drequest);
$headsup_view = id(new PhabricatorHeaderView())
->setHeader('Commit Detail');
$headsup_actions = $this->renderHeadsupActionList($commit, $repository);
$commit_properties = $this->loadCommitProperties(
$commit,
$commit_data,
$parent_query->loadParents()
);
$property_list = id(new PhabricatorPropertyListView())
->setHasKeyboardShortcuts(true);
foreach ($commit_properties as $key => $value) {
$property_list->addProperty($key, $value);
}
$property_list->addTextContent(
phutil_tag(
'div',
array(
'class' => 'diffusion-commit-message phabricator-remarkup',
),
- phutil_safe_html(
- $engine->markupText($commit_data->getCommitMessage()))));
+ $engine->markupText($commit_data->getCommitMessage())));
$content[] = $top_anchor;
$content[] = $headsup_view;
$content[] = $headsup_actions;
$content[] = $property_list;
}
$query = new PhabricatorAuditQuery();
$query->withCommitPHIDs(array($commit->getPHID()));
$audit_requests = $query->execute();
$this->auditAuthorityPHIDs =
PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$content[] = $this->buildAuditTable($commit, $audit_requests);
$content[] = $this->buildComments($commit);
$hard_limit = 1000;
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$change_query->setLimit($hard_limit + 1);
$changes = $change_query->loadChanges();
$was_limited = (count($changes) > $hard_limit);
if ($was_limited) {
$changes = array_slice($changes, 0, $hard_limit);
}
$content[] = $this->buildMergesTable($commit);
$owners_paths = array();
if ($this->highlightedAudits) {
$packages = id(new PhabricatorOwnersPackage())->loadAllWhere(
'phid IN (%Ls)',
mpull($this->highlightedAudits, 'getAuditorPHID'));
if ($packages) {
$owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'repositoryPHID = %s AND packageID IN (%Ld)',
$repository->getPHID(),
mpull($packages, 'getID'));
}
}
$change_table = new DiffusionCommitChangeTableView();
$change_table->setDiffusionRequest($drequest);
$change_table->setPathChanges($changes);
$change_table->setOwnersPaths($owners_paths);
$count = count($changes);
$bad_commit = null;
if ($count == 0) {
$bad_commit = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT * FROM %T WHERE fullCommitName = %s',
PhabricatorRepository::TABLE_BADCOMMIT,
'r'.$callsign.$commit->getCommitIdentifier());
}
if ($bad_commit) {
$error_panel = new AphrontErrorView();
$error_panel->setTitle('Bad Commit');
$error_panel->appendChild($bad_commit['description']);
$content[] = $error_panel;
} else if ($is_foreign) {
// Don't render anything else.
} else if (!count($changes)) {
$no_changes = new AphrontErrorView();
$no_changes->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$no_changes->setTitle('Not Yet Parsed');
// TODO: This can also happen with weird SVN changes that don't do
// anything (or only alter properties?), although the real no-changes case
// is extremely rare and might be impossible to produce organically. We
// should probably write some kind of "Nothing Happened!" change into the
// DB once we parse these changes so we can distinguish between
// "not parsed yet" and "no changes".
$no_changes->appendChild(
"This commit hasn't been fully parsed yet (or doesn't affect any ".
"paths).");
$content[] = $no_changes;
} else if ($was_limited) {
$huge_commit = new AphrontErrorView();
$huge_commit->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$huge_commit->setTitle(pht('Enormous Commit'));
$huge_commit->appendChild(
pht(
'This commit is enormous, and affects more than %d files. '.
'Changes are not shown.',
$hard_limit));
$content[] = $huge_commit;
} else {
$change_panel = new AphrontPanelView();
$change_panel->setHeader("Changes (".number_format($count).")");
$change_panel->setID('toc');
if ($count > self::CHANGES_LIMIT) {
$show_all_button = phutil_tag(
'a',
array(
'class' => 'button green',
'href' => '?show_all=true',
),
'Show All Changes');
$warning_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setTitle('Very Large Commit')
->appendChild(phutil_tag(
'p',
array(),
"This commit is very large. Load each file individually."));
$change_panel->appendChild($warning_view);
$change_panel->addButton($show_all_button);
}
$change_panel->appendChild($change_table);
$change_panel->setNoBackground();
$content[] = $change_panel;
$changesets = DiffusionPathChange::convertToDifferentialChangesets(
$changes);
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$vcs_supports_directory_changes = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$vcs_supports_directory_changes = false;
break;
default:
throw new Exception("Unknown VCS.");
}
$references = array();
foreach ($changesets as $key => $changeset) {
$file_type = $changeset->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
if (!$vcs_supports_directory_changes) {
unset($changesets[$key]);
continue;
}
}
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TODO: Some parts of the views still rely on properties of the
// DifferentialChangeset. Make the objects ephemeral to make sure we don't
// accidentally save them, and then set their ID to the appropriate ID for
// this application (the path IDs).
$path_ids = array_flip(mpull($changes, 'getPath'));
foreach ($changesets as $changeset) {
$changeset->makeEphemeral();
$changeset->setID($path_ids[$changeset->getFilename()]);
}
if ($count <= self::CHANGES_LIMIT) {
$visible_changesets = $changesets;
} else {
$visible_changesets = array();
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'commitPHID = %s AND (auditCommentID IS NOT NULL OR authorPHID = %s)',
$commit->getPHID(),
$user->getPHID());
$path_ids = mpull($inlines, null, 'getPathID');
foreach ($changesets as $key => $changeset) {
if (array_key_exists($changeset->getID(), $path_ids)) {
$visible_changesets[$key] = $changeset;
}
}
}
$change_list_title = DiffusionView::nameCommit(
$repository,
$commit->getCommitIdentifier()
);
$change_list = new DifferentialChangesetListView();
$change_list->setTitle($change_list_title);
$change_list->setChangesets($changesets);
$change_list->setVisibleChangesets($visible_changesets);
$change_list->setRenderingReferences($references);
$change_list->setRenderURI('/diffusion/'.$callsign.'/diff/');
$change_list->setRepository($repository);
$change_list->setUser($user);
// pick the first branch for "Browse in Diffusion" View Option
$branches = $commit_data->getCommitDetail('seenOnBranches', array());
$first_branch = reset($branches);
$change_list->setBranch($first_branch);
$change_list->setStandaloneURI(
'/diffusion/'.$callsign.'/diff/');
$change_list->setRawFileURIs(
// TODO: Implement this, somewhat tricky if there's an octopus merge
// or whatever?
null,
'/diffusion/'.$callsign.'/diff/?view=r');
$change_list->setInlineCommentControllerURI(
'/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
$change_references = array();
foreach ($changesets as $key => $changeset) {
$change_references[$changeset->getID()] = $references[$key];
}
$change_table->setRenderingReferences($change_references);
$content[] = $change_list->render();
}
$content[] = $this->renderAddCommentPanel($commit, $audit_requests);
$commit_id = 'r'.$callsign.$commit->getCommitIdentifier();
$short_name = DiffusionView::nameCommit(
$repository,
$commit->getCommitIdentifier()
);
$crumbs = $this->buildCrumbs(array(
'commit' => true,
));
$prefs = $user->loadPreferences();
$pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE;
$pref_collapse = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED;
$show_filetree = $prefs->getPreference($pref_filetree);
$collapsed = $prefs->getPreference($pref_collapse);
if ($changesets && $show_filetree) {
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setAnchorName('top')
->setTitle($short_name)
->setBaseURI(new PhutilURI('/'.$commit_id))
->build($changesets)
->setCrumbs($crumbs)
->setCollapsed((bool)$collapsed)
->appendChild($content);
$content = $nav;
} else {
$content = array($crumbs, $content);
}
return $this->buildApplicationPage(
$content,
array(
'title' => $commit_id
)
);
}
private function loadCommitProperties(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data,
array $parents) {
assert_instances_of($parents, 'PhabricatorRepositoryCommit');
$user = $this->getRequest()->getUser();
$commit_phid = $commit->getPHID();
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($commit_phid))
->withEdgeTypes(array(
PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT
))
->execute();
$task_phids = array_keys(
$edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK]
);
$proj_phids = array_keys(
$edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]
);
$phids = array_merge($task_phids, $proj_phids);
if ($data->getCommitDetail('authorPHID')) {
$phids[] = $data->getCommitDetail('authorPHID');
}
if ($data->getCommitDetail('reviewerPHID')) {
$phids[] = $data->getCommitDetail('reviewerPHID');
}
if ($data->getCommitDetail('committerPHID')) {
$phids[] = $data->getCommitDetail('committerPHID');
}
if ($data->getCommitDetail('differential.revisionPHID')) {
$phids[] = $data->getCommitDetail('differential.revisionPHID');
}
if ($parents) {
foreach ($parents as $parent) {
$phids[] = $parent->getPHID();
}
}
$handles = array();
if ($phids) {
$handles = $this->loadViewerHandles($phids);
}
$props = array();
if ($commit->getAuditStatus()) {
$status = PhabricatorAuditCommitStatusConstants::getStatusName(
$commit->getAuditStatus());
$props['Status'] = phutil_tag(
'strong',
array(),
$status);
}
$props['Committed'] = phabricator_datetime($commit->getEpoch(), $user);
$author_phid = $data->getCommitDetail('authorPHID');
if ($data->getCommitDetail('authorPHID')) {
$props['Author'] = $handles[$author_phid]->renderLink();
} else {
$props['Author'] = $data->getAuthorName();
}
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
if ($reviewer_phid) {
$props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
}
$committer = $data->getCommitDetail('committer');
if ($committer) {
$committer_phid = $data->getCommitDetail('committerPHID');
if ($data->getCommitDetail('committerPHID')) {
$props['Committer'] = $handles[$committer_phid]->renderLink();
} else {
$props['Committer'] = $committer;
}
}
$revision_phid = $data->getCommitDetail('differential.revisionPHID');
if ($revision_phid) {
$props['Differential Revision'] = $handles[$revision_phid]->renderLink();
}
if ($parents) {
$parent_links = array();
foreach ($parents as $parent) {
$parent_links[] = $handles[$parent->getPHID()]->renderLink();
}
$props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links);
}
$request = $this->getDiffusionRequest();
$props['Branches'] = phutil_tag(
'span',
array(
'id' => 'commit-branches',
),
'Unknown');
$props['Tags'] = phutil_tag(
'span',
array(
'id' => 'commit-tags',
),
'Unknown');
$callsign = $request->getRepository()->getCallsign();
$root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier();
Javelin::initBehavior(
'diffusion-commit-branches',
array(
$root.'/branches/' => 'commit-branches',
$root.'/tags/' => 'commit-tags',
));
$refs = $this->buildRefs($request);
if ($refs) {
$props['References'] = $refs;
}
if ($task_phids) {
$task_list = array();
foreach ($task_phids as $phid) {
$task_list[] = $handles[$phid]->renderLink();
}
$task_list = phutil_implode_html(phutil_tag('br'), $task_list);
$props['Tasks'] = $task_list;
}
if ($proj_phids) {
$proj_list = array();
foreach ($proj_phids as $phid) {
$proj_list[] = $handles[$phid]->renderLink();
}
$proj_list = phutil_implode_html(phutil_tag('br'), $proj_list);
$props['Projects'] = $proj_list;
}
return $props;
}
private function buildAuditTable(
PhabricatorRepositoryCommit $commit,
array $audits) {
assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$view = new PhabricatorAuditListView();
$view->setAudits($audits);
$view->setCommits(array($commit));
$view->setUser($user);
$view->setShowDescriptions(false);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$view->setAuthorityPHIDs($this->auditAuthorityPHIDs);
$this->highlightedAudits = $view->getHighlightedAudits();
$panel = new AphrontPanelView();
$panel->setHeader('Audits');
$panel->setCaption('Audits you are responsible for are highlighted.');
$panel->appendChild($view);
$panel->setNoBackground();
return $panel;
}
private function buildComments(PhabricatorRepositoryCommit $commit) {
$user = $this->getRequest()->getUser();
$comments = id(new PhabricatorAuditComment())->loadAllWhere(
'targetPHID = %s ORDER BY dateCreated ASC',
$commit->getPHID());
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'commitPHID = %s AND auditCommentID IS NOT NULL',
$commit->getPHID());
$path_ids = mpull($inlines, 'getPathID');
$path_map = array();
if ($path_ids) {
$path_map = id(new DiffusionPathQuery())
->withPathIDs($path_ids)
->execute();
$path_map = ipull($path_map, 'path', 'id');
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
foreach ($comments as $comment) {
$engine->addObject(
$comment,
PhabricatorAuditComment::MARKUP_FIELD_BODY);
}
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$view = new DiffusionCommentListView();
$view->setMarkupEngine($engine);
$view->setUser($user);
$view->setComments($comments);
$view->setInlineComments($inlines);
$view->setPathMap($path_map);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
return $view;
}
private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'diffusion-audit-'.$commit->getID());
if ($draft) {
$draft = $draft->getDraft();
} else {
$draft = null;
}
$actions = $this->getAuditActions($commit, $audit_requests);
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/audit/addcomment/')
->addHiddenInput('commit', $commit->getPHID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Action')
->setName('action')
->setID('audit-action')
->setOptions($actions))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add Auditors')
->setName('auditors')
->setControlID('add-auditors')
->setControlStyle('display: none')
->setID('add-auditors-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add CCs')
->setName('ccs')
->setControlID('add-ccs')
->setControlStyle('display: none')
->setID('add-ccs-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Comments')
->setName('content')
->setValue($draft)
->setID('audit-content')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Cook the Books'));
$panel = new AphrontPanelView();
$panel->setHeader($is_serious ? 'Audit Commit' : 'Creative Accounting');
$panel->appendChild($form);
$panel->addClass('aphront-panel-accent');
$panel->addClass('aphront-panel-flush');
require_celerity_resource('phabricator-transaction-view-css');
Javelin::initBehavior(
'differential-add-reviewers-and-ccs',
array(
'dynamic' => array(
'add-auditors-tokenizer' => array(
'actions' => array('add_auditors' => 1),
'src' => '/typeahead/common/users/',
'row' => 'add-auditors',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user name...',
),
'add-ccs-tokenizer' => array(
'actions' => array('add_ccs' => 1),
'src' => '/typeahead/common/mailable/',
'row' => 'add-ccs',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user or mailing list...',
),
),
'select' => 'audit-action',
));
Javelin::initBehavior('differential-feedback-preview', array(
'uri' => '/audit/preview/'.$commit->getID().'/',
'preview' => 'audit-preview',
'content' => 'audit-content',
'action' => 'audit-action',
'previewTokenizers' => array(
'auditors' => 'add-auditors-tokenizer',
'ccs' => 'add-ccs-tokenizer',
),
'inline' => 'inline-comment-preview',
'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/',
));
$preview_panel = hsprintf(
'
');
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
return phutil_tag(
'div',
array(
'id' => $pane_id,
),
hsprintf(
'',
id(new PhabricatorAnchorView())
->setAnchorName('comment')
->setNavigationMarker(true)
->render(),
$panel->render(),
$preview_panel));
}
/**
* Return a map of available audit actions for rendering into a .
* This shows the user valid actions, and does not show nonsense/invalid
* actions (like closing an already-closed commit, or resigning from a commit
* you have no association with).
*/
private function getAuditActions(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$user_is_author = ($commit->getAuthorPHID() == $user->getPHID());
$user_request = null;
foreach ($audit_requests as $audit_request) {
if ($audit_request->getAuditorPHID() == $user->getPHID()) {
$user_request = $audit_request;
break;
}
}
$actions = array();
$actions[PhabricatorAuditActionConstants::COMMENT] = true;
$actions[PhabricatorAuditActionConstants::ADD_CCS] = true;
$actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true;
// We allow you to accept your own commits. A use case here is that you
// notice an issue with your own commit and "Raise Concern" as an indicator
// to other auditors that you're on top of the issue, then later resolve it
// and "Accept". You can not accept on behalf of projects or packages,
// however.
$actions[PhabricatorAuditActionConstants::ACCEPT] = true;
$actions[PhabricatorAuditActionConstants::CONCERN] = true;
// To resign, a user must have authority on some request and not be the
// commit's author.
if (!$user_is_author) {
$may_resign = false;
$authority_map = array_fill_keys($this->auditAuthorityPHIDs, true);
foreach ($audit_requests as $request) {
if (empty($authority_map[$request->getAuditorPHID()])) {
continue;
}
$may_resign = true;
break;
}
// If the user has already resigned, don't show "Resign...".
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
if ($user_request) {
if ($user_request->getAuditStatus() == $status_resigned) {
$may_resign = false;
}
}
if ($may_resign) {
$actions[PhabricatorAuditActionConstants::RESIGN] = true;
}
}
$status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
$concern_raised = ($commit->getAuditStatus() == $status_concern);
$can_close_option = PhabricatorEnv::getEnvConfig(
'audit.can-author-close-audit');
if ($can_close_option && $user_is_author && $concern_raised) {
$actions[PhabricatorAuditActionConstants::CLOSE] = true;
}
foreach ($actions as $constant => $ignored) {
$actions[$constant] =
PhabricatorAuditActionConstants::getActionName($constant);
}
return $actions;
}
private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
$drequest = $this->getDiffusionRequest();
$limit = 50;
$merge_query = DiffusionMergedCommitsQuery::newFromDiffusionRequest(
$drequest);
$merge_query->setLimit($limit + 1);
$merges = $merge_query->loadMergedCommits();
if (!$merges) {
return null;
}
$caption = null;
if (count($merges) > $limit) {
$merges = array_slice($merges, 0, $limit);
$caption =
"This commit merges more than {$limit} changes. Only the first ".
"{$limit} are shown.";
}
$history_table = new DiffusionHistoryTableView();
$history_table->setUser($this->getRequest()->getUser());
$history_table->setDiffusionRequest($drequest);
$history_table->setHistory($merges);
$history_table->loadRevisions();
$phids = $history_table->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$history_table->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader('Merged Changes');
$panel->setCaption($caption);
$panel->appendChild($history_table);
$panel->setNoBackground();
return $panel;
}
private function renderHeadsupActionList(
PhabricatorRepositoryCommit $commit,
PhabricatorRepository $repository) {
$request = $this->getRequest();
$user = $request->getUser();
$actions = id(new PhabricatorActionListView())
->setUser($user)
->setObject($commit);
// TODO -- integrate permissions into whether or not this action is shown
$uri = '/diffusion/'.$repository->getCallSign().'/commit/'.
$commit->getCommitIdentifier().'/edit/';
$action = id(new PhabricatorActionView())
->setName('Edit Commit')
->setHref($uri)
->setIcon('edit');
$actions->addAction($action);
require_celerity_resource('phabricator-object-selector-css');
require_celerity_resource('javelin-behavior-phabricator-object-selector');
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$action = id(new PhabricatorActionView())
->setName('Edit Maniphest Tasks')
->setIcon('attach')
->setHref('/search/attach/'.$commit->getPHID().'/TASK/edge/')
->setWorkflow(true);
$actions->addAction($action);
}
if ($user->getIsAdmin()) {
$action = id(new PhabricatorActionView())
->setName('MetaMTA Transcripts')
->setIcon('file')
->setHref('/mail/?phid='.$commit->getPHID());
$actions->addAction($action);
}
$action = id(new PhabricatorActionView())
->setName('Herald Transcripts')
->setIcon('file')
->setHref('/herald/transcript/?phid='.$commit->getPHID())
->setWorkflow(true);
$actions->addAction($action);
$action = id(new PhabricatorActionView())
->setName('Download Raw Diff')
->setHref($request->getRequestURI()->alter('diff', true))
->setIcon('download');
$actions->addAction($action);
return $actions;
}
private function buildRefs(DiffusionRequest $request) {
// Not turning this into a proper Query class since it's pretty simple,
// one-off, and Git-specific.
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$repository = $request->getRepository();
if ($repository->getVersionControlSystem() != $type_git) {
return null;
}
list($stdout) = $repository->execxLocalCommand(
'log --format=%s -n 1 %s --',
'%d',
$request->getCommit());
// %d, gives a weird output format
// similar to (remote/one, remote/two, remote/three)
$refs = trim($stdout, "() \n");
if (!$refs) {
return null;
}
$refs = explode(',', $refs);
$refs = array_map('trim', $refs);
$ref_links = array();
foreach ($refs as $ref) {
$ref_links[] = phutil_tag(
'a',
array(
'href' => $request->generateURI(
array(
'action' => 'browse',
'branch' => $ref,
)),
),
$ref);
}
return phutil_implode_html(', ', $ref_links);
}
private function buildRawDiffResponse(DiffusionRequest $drequest) {
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$raw_diff = $raw_query->loadRawDiff();
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $drequest->getCommit().'.diff',
));
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
}
diff --git a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
index 859941bd0..6a439ab2c 100644
--- a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
@@ -1,145 +1,145 @@
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$repository = $request->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// TODO: Verify local-path?
$query = new DiffusionGitBrowseQuery();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$query = new DiffusionMercurialBrowseQuery();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$query = new DiffusionSvnBrowseQuery();
break;
default:
throw new Exception("Unsupported VCS!");
}
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function getReasonForEmptyResultSet() {
return $this->reason;
}
final public function getExistedAtCommit() {
return $this->existedAtCommit;
}
final public function getDeletedAtCommit() {
return $this->deletedAtCommit;
}
final public function loadPaths() {
return $this->executeQuery();
}
final public function shouldOnlyTestValidity() {
return $this->validityOnly;
}
final public function needValidityOnly($need_validity_only) {
$this->validityOnly = $need_validity_only;
return $this;
}
final public function renderReadme(array $results) {
$drequest = $this->getRequest();
$readme = null;
foreach ($results as $result) {
$file_type = $result->getFileType();
if (($file_type != ArcanistDiffChangeType::FILE_NORMAL) &&
($file_type != ArcanistDiffChangeType::FILE_TEXT)) {
// Skip directories, etc.
continue;
}
$path = $result->getPath();
if (preg_match('/^readme(|\.txt|\.remarkup|\.rainbow)$/i', $path)) {
$readme = $result;
break;
}
}
if (!$readme) {
return null;
}
$readme_request = DiffusionRequest::newFromDictionary(
array(
'repository' => $drequest->getRepository(),
'commit' => $drequest->getStableCommitName(),
'path' => $readme->getFullPath(),
));
$content_query = DiffusionFileContentQuery::newFromDiffusionRequest(
$readme_request);
$content_query->loadFileContent();
$readme_content = $content_query->getRawData();
if (preg_match('/\\.txt$/', $readme->getPath())) {
$readme_content = phutil_escape_html_newlines($readme_content);
$class = null;
} else if (preg_match('/\\.rainbow$/', $readme->getPath())) {
$highlighter = new PhutilRainbowSyntaxHighlighter();
$readme_content = $highlighter
->getHighlightFuture($readme_content)
->resolve();
$readme_content = phutil_escape_html_newlines($readme_content);
require_celerity_resource('syntax-highlighting-css');
$class = 'remarkup-code';
} else {
// Markup extensionless files as remarkup so we get links and such.
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
- $readme_content = phutil_safe_html($engine->markupText($readme_content));
+ $readme_content = $engine->markupText($readme_content);
$class = 'phabricator-remarkup';
}
$readme_content = phutil_tag(
'div',
array(
'class' => $class,
),
$readme_content);
return $readme_content;
}
abstract protected function executeQuery();
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php
index d16f5d948..3bba99426 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php
@@ -1,243 +1,241 @@
username = idx($data, 'username');
$this->page = idx($data, 'page');
}
public function getProfileUser() {
return $this->profileUser;
}
private function getMainFilters($username) {
return array(
array(
'key' => 'feed',
'name' => pht('Feed'),
'href' => '/p/'.$username.'/feed/'
),
array(
'key' => 'about',
'name' => pht('About'),
'href' => '/p/'.$username.'/about/'
)
);
}
public function processRequest() {
$viewer = $this->getRequest()->getUser();
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$this->username);
if (!$user) {
return new Aphront404Response();
}
$this->profileUser = $user;
require_celerity_resource('phabricator-profile-css');
$profile = id(new PhabricatorUserProfile())->loadOneWhere(
'userPHID = %s',
$user->getPHID());
if (!$profile) {
$profile = new PhabricatorUserProfile();
}
$username = phutil_escape_uri($user->getUserName());
$menu = new PhabricatorMenuView();
foreach ($this->getMainFilters($username) as $filter) {
$menu->newLink($filter['name'], $filter['href'], $filter['key']);
}
$menu->newLabel(pht('Activity'), 'activity');
// NOTE: applications install the various links through PhabricatorEvent
// listeners
$oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere(
'userID = %d',
$user->getID());
$oauths = mpull($oauths, null, 'getOAuthProvider');
$providers = PhabricatorOAuthProvider::getAllProviders();
$added_label = false;
foreach ($providers as $provider) {
if (!$provider->isProviderEnabled()) {
continue;
}
$provider_key = $provider->getProviderKey();
if (!isset($oauths[$provider_key])) {
continue;
}
$name = pht('%s Profile', $provider->getProviderName());
$href = $oauths[$provider_key]->getAccountURI();
if ($href) {
if (!$added_label) {
$menu->newLabel(pht('Linked Accounts'), 'linked_accounts');
$added_label = true;
}
$menu->addMenuItem(
id(new PhabricatorMenuItemView())
->setIsExternal(true)
->setName($name)
->setHref($href)
->setType(PhabricatorMenuItemView::TYPE_LINK)
);
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_PEOPLE_DIDRENDERMENU,
array(
'menu' => $menu,
'person' => $user,
));
$event->setUser($viewer);
PhutilEventEngine::dispatchEvent($event);
$nav = AphrontSideNavFilterView::newFromMenu($event->getValue('menu'));
$this->page = $nav->selectFilter($this->page, 'feed');
switch ($this->page) {
case 'feed':
$content = $this->renderUserFeed($user);
break;
case 'about':
$content = $this->renderBasicInformation($user, $profile);
break;
default:
throw new Exception("Unknown page '{$this->page}'!");
}
$picture = $user->loadProfileImageURI();
$header = new PhabricatorProfileHeaderView();
$header
->setProfilePicture($picture)
->setName($user->getUserName().' ('.$user->getRealName().')')
->setDescription($profile->getTitle());
if ($user->getIsDisabled()) {
$header->setStatus('Disabled');
} else {
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
array($user->getPHID()));
if ($statuses) {
$header->setStatus(reset($statuses)->getTerseSummary($viewer));
}
}
$nav->appendChild($header);
$content = ''.$content.'
';
$header->appendChild($content);
if ($user->getPHID() == $viewer->getPHID()) {
$nav->addFilter(
null,
pht('Edit Profile...'),
'/settings/panel/profile/'
);
}
if ($viewer->getIsAdmin()) {
$nav->addFilter(
null,
pht('Administrate User...'),
'/people/edit/'.$user->getID().'/'
);
}
return $this->buildApplicationPage(
$nav,
array(
'title' => $user->getUsername(),
));
}
private function renderBasicInformation($user, $profile) {
$blurb = nonempty(
$profile->getBlurb(),
- '//'.
- pht('Nothing is known about this rare specimen.')
- .'//'
+ '//'.pht('Nothing is known about this rare specimen.').'//'
);
$engine = PhabricatorMarkupEngine::newProfileMarkupEngine();
- $blurb = phutil_safe_html($engine->markupText($blurb));
+ $blurb = $engine->markupText($blurb);
$viewer = $this->getRequest()->getUser();
$content = hsprintf(
''.
'',
$user->getPHID(),
phabricator_datetime($user->getDateCreated(), $viewer),
$blurb);
return $content;
}
private function renderUserFeed(PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$user->getPHID(),
));
$query->setLimit(100);
$query->setViewer($viewer);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($viewer);
$view = $builder->buildView();
return
'';
}
}
diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php
index 6c13c55fb..ebc252cba 100644
--- a/src/applications/phriction/storage/PhrictionContent.php
+++ b/src/applications/phriction/storage/PhrictionContent.php
@@ -1,103 +1,102 @@
shouldUseMarkupCache($field)) {
$id = $this->getID();
} else {
$id = PhabricatorHash::digest($this->getMarkupText($field));
}
return "phriction:{$field}:{$id}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getContent();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhrictionMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
$toc = PhutilRemarkupEngineRemarkupHeaderBlockRule::renderTableOfContents(
$engine);
if ($toc) {
- $toc =
+ $toc = hsprintf(
'';
+ ''.
+ '%s'.
+ '',
+ pht('Table of Contents'),
+ $toc);
}
- return
- '';
+ return hsprintf(
+ '',
+ $toc,
+ $output);
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php b/src/applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php
index 7d51f23d2..fd827987f 100644
--- a/src/applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php
+++ b/src/applications/remarkup/conduit/ConduitAPI_remarkup_process_Method.php
@@ -1,67 +1,67 @@
'Content may not be empty.',
'ERR-INVALID-ENGINE' => 'Invalid markup engine.',
);
}
public function defineParamTypes() {
$available_contexts = array_keys($this->getEngineContexts());
$available_contexts = implode(', ', $available_contexts);
return array(
'context' => 'required enum<'.$available_contexts.'>',
'content' => 'required string',
);
}
protected function execute(ConduitAPIRequest $request) {
$content = $request->getValue('content');
$context = $request->getValue('context');
$engine_class = idx($this->getEngineContexts(), $context);
if (!$engine_class) {
throw new ConduitException('ERR-INVALID_ENGINE');
}
$engine = PhabricatorMarkupEngine::$engine_class();
$engine->setConfig('viewer', $request->getUser());
$text = $engine->markupText($content);
if ($text) {
- $content = phutil_safe_html($text)->getHTMLContent();
+ $content = hsprintf('%s', $text)->getHTMLContent();
} else {
$content = '';
}
$result = array(
'content' => $content,
);
return $result;
}
private function getEngineContexts() {
return array(
'phriction' => 'newPhrictionMarkupEngine',
'maniphest' => 'newManiphestMarkupEngine',
'differential' => 'newDifferentialMarkupEngine',
);
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
index 5285ff606..f2fe76132 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
@@ -1,459 +1,458 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_phid = $user->getPHID();
$poll = id(new PhabricatorSlowvotePoll())->load($this->id);
if (!$poll) {
return new Aphront404Response();
}
$options = id(new PhabricatorSlowvoteOption())->loadAllWhere(
'pollID = %d',
$poll->getID());
$choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere(
'pollID = %d',
$poll->getID());
$comments = id(new PhabricatorSlowvoteComment())->loadAllWhere(
'pollID = %d',
$poll->getID());
$choices_by_option = mgroup($choices, 'getOptionID');
$comments_by_user = mpull($comments, null, 'getAuthorPHID');
$choices_by_user = mgroup($choices, 'getAuthorPHID');
$viewer_choices = idx($choices_by_user, $viewer_phid, array());
$viewer_comment = idx($comments_by_user, $viewer_phid, null);
$comment_text = null;
if ($viewer_comment) {
$comment_text = $viewer_comment->getCommentText();
}
if ($request->isFormPost()) {
$comment = idx($comments_by_user, $viewer_phid, null);
if ($comment) {
$comment->delete();
}
$comment_text = $request->getStr('comments');
if (strlen($comment_text)) {
id(new PhabricatorSlowvoteComment())
->setAuthorPHID($viewer_phid)
->setPollID($poll->getID())
->setCommentText($comment_text)
->save();
}
$votes = $request->getArr('vote');
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
// Enforce only one vote.
$votes = array_slice($votes, 0, 1);
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// No filtering.
break;
default:
throw new Exception("Unknown poll method!");
}
foreach ($viewer_choices as $viewer_choice) {
$viewer_choice->delete();
}
foreach ($votes as $vote) {
id(new PhabricatorSlowvoteChoice())
->setAuthorPHID($viewer_phid)
->setPollID($poll->getID())
->setOptionID($vote)
->save();
}
return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID());
}
require_celerity_resource('phabricator-slowvote-css');
$phids = array_merge(
mpull($choices, 'getAuthorPHID'),
mpull($comments, 'getAuthorPHID'),
array(
$poll->getAuthorPHID(),
));
$query = new PhabricatorObjectHandleData($phids);
$handles = $query->loadHandles();
$objects = $query->loadObjects();
if ($poll->getShuffle()) {
shuffle($options);
}
$option_markup = array();
foreach ($options as $option) {
$option_markup[] = $this->renderPollOption(
$poll,
$viewer_choices,
$option);
}
$comments_by_option = array();
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
$choice_ids = array();
foreach ($choices_by_user as $user_phid => $user_choices) {
$choice_ids[$user_phid] = head($user_choices)->getOptionID();
}
foreach ($comments as $comment) {
$choice = idx($choice_ids, $comment->getAuthorPHID());
if ($choice) {
$comments_by_option[$choice][] = $comment;
}
}
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// All comments are grouped in approval voting.
break;
default:
throw new Exception("Unknown poll method!");
}
$result_markup = $this->renderResultMarkup(
$poll,
$options,
$choices,
$comments,
$viewer_choices,
$choices_by_option,
$comments_by_option,
$handles,
$objects);
if ($viewer_choices) {
$instructions =
'Your vote has been recorded... but there is still ample time to '.
'rethink your position. Have you thoroughly considered all possible '.
'eventualities?';
} else {
$instructions =
'This is a weighty matter indeed. Consider your choices with the '.
'greatest of care.';
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(hsprintf(
'%s
',
$instructions))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Vote')
->setValue($option_markup))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Comments')
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
->setName('comments')
->setValue($comment_text))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Cautiously Engage in Deliberations'));
$panel = new AphrontPanelView();
$panel->setHeader($poll->getQuestion());
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->appendChild($form);
$panel->appendChild('
');
$panel->appendChild($result_markup);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'V'.$poll->getID().' '.$poll->getQuestion(),
));
}
private function renderComments(array $comments, array $handles) {
assert_instances_of($comments, 'PhabricatorSlowvoteComment');
assert_instances_of($handles, 'PhabricatorObjectHandle');
$viewer = $this->getRequest()->getUser();
$engine = PhabricatorMarkupEngine::newSlowvoteMarkupEngine();
$comment_markup = array();
foreach ($comments as $comment) {
$handle = $handles[$comment->getAuthorPHID()];
- $markup = phutil_safe_html(
- $engine->markupText($comment->getCommentText()));
+ $markup = $engine->markupText($comment->getCommentText());
require_celerity_resource('phabricator-remarkup-css');
$comment_markup[] = hsprintf(
''.
''.
'%s'.
' %s '.
' | '.
''.
''.
' | '.
'
',
$handle->renderLink(),
phabricator_datetime($comment->getDateCreated(), $viewer),
$markup);
}
if ($comment_markup) {
$comment_markup = phutil_tag(
'table',
array(
'class' => 'phabricator-slowvote-comments',
),
$comment_markup);
} else {
$comment_markup = null;
}
return $comment_markup;
}
private function renderPollOption(
PhabricatorSlowvotePoll $poll,
array $viewer_choices,
PhabricatorSlowvoteOption $option) {
assert_instances_of($viewer_choices, 'PhabricatorSlowvoteChoice');
$id = $option->getID();
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
// Render a radio button.
$selected_option = head($viewer_choices);
if ($selected_option) {
$selected = $selected_option->getOptionID();
} else {
$selected = null;
}
if ($selected == $id) {
$checked = "checked";
} else {
$checked = null;
}
$input = phutil_tag(
'input',
array(
'type' => 'radio',
'name' => 'vote[]',
'value' => $id,
'checked' => $checked,
));
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// Render a check box.
$checked = null;
foreach ($viewer_choices as $choice) {
if ($choice->getOptionID() == $id) {
$checked = 'checked';
break;
}
}
$input = phutil_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'vote[]',
'checked' => $checked,
'value' => $id,
));
break;
default:
throw new Exception("Unknown poll method!");
}
if ($checked) {
$checked_class = 'phabricator-slowvote-checked';
} else {
$checked_class = null;
}
return phutil_tag(
'label',
array(
'class' => 'phabricator-slowvote-label '.$checked_class,
),
array($input, $option->getName()));
}
private function renderVoteCount(
PhabricatorSlowvotePoll $poll,
array $choices,
array $chosen) {
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($chosen, 'PhabricatorSlowvoteChoice');
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
$out_of_total = count($choices);
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// Count unique respondents for approval votes.
$out_of_total = count(mpull($choices, null, 'getAuthorPHID'));
break;
default:
throw new Exception("Unknown poll method!");
}
return sprintf(
'%d / %d (%d%%)',
number_format(count($chosen)),
number_format($out_of_total),
$out_of_total
? round(100 * count($chosen) / $out_of_total)
: 0);
}
private function renderResultMarkup(
PhabricatorSlowvotePoll $poll,
array $options,
array $choices,
array $comments,
array $viewer_choices,
array $choices_by_option,
array $comments_by_option,
array $handles,
array $objects) {
assert_instances_of($options, 'PhabricatorSlowvoteOption');
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($comments, 'PhabricatorSlowvoteComment');
assert_instances_of($viewer_choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($handles, 'PhabricatorObjectHandle');
assert_instances_of($objects, 'PhabricatorLiskDAO');
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$can_see_responses = false;
$need_vote = false;
switch ($poll->getResponseVisibility()) {
case PhabricatorSlowvotePoll::RESPONSES_VISIBLE:
$can_see_responses = true;
break;
case PhabricatorSlowvotePoll::RESPONSES_VOTERS:
$can_see_responses = (bool)$viewer_choices;
$need_vote = true;
break;
case PhabricatorSlowvotePoll::RESPONSES_OWNER:
$can_see_responses = ($viewer_phid == $poll->getAuthorPHID());
break;
}
$result_markup = id(new AphrontFormLayoutView())
->appendChild(phutil_tag('h1', array(), 'Ongoing Deliberation'));
if (!$can_see_responses) {
if ($need_vote) {
$reason = "You must vote to see the results.";
} else {
$reason = "The results are not public.";
}
$result_markup
->appendChild(hsprintf(
'%s
',
$reason));
return $result_markup;
}
foreach ($options as $option) {
$id = $option->getID();
$chosen = idx($choices_by_option, $id, array());
$users = array_select_keys($handles, mpull($chosen, 'getAuthorPHID'));
if ($users) {
$user_markup = array();
foreach ($users as $handle) {
$object = idx($objects, $handle->getPHID());
if (!$object) {
continue;
}
$profile_image = $handle->getImageURI();
$user_markup[] = phutil_tag(
'a',
array(
'href' => $handle->getURI(),
'class' => 'phabricator-slowvote-facepile',
),
phutil_tag(
'img',
array(
'src' => $profile_image,
)));
}
} else {
$user_markup = 'This option has failed to appeal to anyone.';
}
$comment_markup = $this->renderComments(
idx($comments_by_option, $id, array()),
$handles);
$vote_count = $this->renderVoteCount(
$poll,
$choices,
$chosen);
$result_markup->appendChild(hsprintf(
''.
'
%s
'.
'
%s
'.
'
'.
'%s'.
'
'.
'
'.
'%s'.
'
',
$vote_count,
$option->getName(),
phutil_tag('div', array(), $user_markup),
$comment_markup));
}
if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_APPROVAL &&
$comments) {
$comment_markup = $this->renderComments(
$comments,
$handles);
$result_markup->appendChild(
phutil_tag('h1', array(), 'Motions Proposed for Consideration'));
$result_markup->appendChild($comment_markup);
}
return $result_markup;
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 5e9a96d31..f242519c1 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,549 +1,547 @@
addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine {
private $objects = array();
private $viewer;
- private $version = 2;
+ private $version = 3;
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
// Finalize the output.
foreach ($objects as $key => $info) {
$data = $blocks[$key]->getCacheData();
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($data);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
if (empty($this->objects[$key])) {
throw new Exception(
"Call addObject() before getOutput() (key = '{$key}').");
}
if (!isset($this->objects[$key]['output'])) {
throw new Exception(
"Call process() before getOutput().");
}
- return new PhutilSafeHTML($this->objects[$key]['output']);
+ return $this->objects[$key]['output'];
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
return $object->getMarkupFieldKey($field).'@'.$this->version;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
try {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
} catch (Exception $ex) {
phlog($ex);
}
}
foreach ($objects as $key => $info) {
if (isset($blocks[$key])) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key])) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$blocks[$key]->replace();
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(array(
'macros' => false,
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'custom-inline' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-rules'),
'custom-block' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-block-rules'),
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newProfileMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newSlowvoteMarkupEngine() {
return self::newMarkupEngine(array(
));
}
public static function newPonderMarkupEngine(array $options = array()) {
return self::newMarkupEngine($options);
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'custom-inline' => array(),
'custom-block' => array(),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
);
}
/**
* @task engine
*/
public static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', true);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$rules = array();
$rules[] = new PhutilRemarkupRuleEscapeRemarkup();
$rules[] = new PhutilRemarkupRuleMonospace();
$custom_rule_classes = $options['custom-inline'];
if ($custom_rule_classes) {
foreach ($custom_rule_classes as $custom_rule_class) {
$rules[] = newv($custom_rule_class, array());
}
}
$rules[] = new PhutilRemarkupRuleDocumentLink();
if ($options['youtube']) {
$rules[] = new PhabricatorRemarkupRuleYoutube();
}
$rules[] = new PhutilRemarkupRuleHyperlink();
$rules[] = new PhabricatorRemarkupRulePhriction();
$rules[] = new PhabricatorRemarkupRuleDifferentialHandle();
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$rules[] = new PhabricatorRemarkupRuleManiphestHandle();
}
$rules[] = new PhabricatorRemarkupRuleEmbedFile();
$rules[] = new PhabricatorRemarkupRuleDifferential();
$rules[] = new PhabricatorRemarkupRuleDiffusion();
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$rules[] = new PhabricatorRemarkupRuleManiphest();
}
$rules[] = new PhabricatorRemarkupRulePaste();
$rules[] = new PhabricatorRemarkupRuleCountdown();
$rules[] = new PonderRuleQuestion();
if ($options['macros']) {
$rules[] = new PhabricatorRemarkupRuleImageMacro();
$rules[] = new PhabricatorRemarkupRuleMeme();
}
$rules[] = new PhabricatorRemarkupRuleMention();
- $rules[] = new PhutilRemarkupRuleEscapeHTML();
$rules[] = new PhutilRemarkupRuleBold();
$rules[] = new PhutilRemarkupRuleItalic();
$rules[] = new PhutilRemarkupRuleDel();
$blocks = array();
$blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule();
$custom_block_rule_classes = $options['custom-block'];
if ($custom_block_rule_classes) {
foreach ($custom_block_rule_classes as $custom_block_rule_class) {
$blocks[] = newv($custom_block_rule_class, array());
}
}
foreach ($blocks as $block) {
if ($block instanceof PhutilRemarkupEngineRemarkupLiteralBlockRule) {
$literal_rules = array();
- $literal_rules[] = new PhutilRemarkupRuleEscapeHTML();
$literal_rules[] = new PhutilRemarkupRuleLinebreaks();
$block->setMarkupRules($literal_rules);
} else if (
!($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) {
$block->setMarkupRules($rules);
}
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorRemarkupRuleMention::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function extractFilePHIDsFromEmbeddedFiles(
array $content_blocks) {
$files = array();
$engine = self::newDifferentialMarkupEngine();
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$ids = $engine->getTextMetadata(
PhabricatorRemarkupRuleEmbedFile::KEY_EMBED_FILE_PHIDS,
array());
$files += $ids;
}
return $files;
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", trim($corpus));
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
index 548a2b1d6..3356aed8e 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
@@ -1,74 +1,71 @@
replaceHTML(
"@\B{C(\d+)}\B@",
array($this, 'markupCountdown'),
$text);
}
- private function markupCountdown($matches) {
+ protected function markupCountdown($matches) {
$countdown = id(new PhabricatorTimer())->load($matches[1]);
if (!$countdown) {
return $matches[0];
}
$id = celerity_generate_unique_node_id();
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_COUNTDOWN;
$metadata = $engine->getTextMetadata($metadata_key, array());
$metadata[$id] = array($countdown->getDatepoint(), $token);
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_COUNTDOWN;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
require_celerity_resource('javelin-behavior-countdown-timer');
foreach ($metadata as $id => $info) {
list($time, $token) = $info;
+ $prefix = 'phabricator-timer-';
$count = phutil_tag(
'span',
array(
'id' => $id,
),
array(
- javelin_tag('span',
- array('sigil' => 'phabricator-timer-days'), '').'d',
- javelin_tag('span',
- array('sigil' => 'phabricator-timer-hours'), '').'h',
- javelin_tag('span',
- array('sigil' => 'phabricator-timer-minutes'), '').'m',
- javelin_tag('span',
- array('sigil' => 'phabricator-timer-seconds'), '').'s',
+ javelin_tag('span', array('sigil' => $prefix.'days'), ''), 'd',
+ javelin_tag('span', array('sigil' => $prefix.'hours'), ''), 'h',
+ javelin_tag('span', array('sigil' => $prefix.'minutes'), ''), 'm',
+ javelin_tag('span', array('sigil' => $prefix.'seconds'), ''), 's',
));
Javelin::initBehavior('countdown-timer', array(
'timestamp' => $time,
'container' => $id,
));
$engine->overwriteStoredText($token, $count);
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
index 9ef0ac8ad..e7c25dd1f 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
@@ -1,187 +1,187 @@
replaceHTML(
"@{F(\d+)([^}]+?)?}@",
array($this, 'markupEmbedFile'),
$text);
}
public function markupEmbedFile($matches) {
$file = null;
if ($matches[1]) {
// TODO: This is pretty inefficient if there are a bunch of files.
$file = id(new PhabricatorFile())->load($matches[1]);
}
if (!$file) {
return $matches[0];
}
$phid = $file->getPHID();
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_EMBED_FILE;
$metadata = $engine->getTextMetadata($metadata_key, array());
$bundle = array('token' => $token);
$options = array(
'size' => 'thumb',
'layout' => 'left',
'float' => false,
'name' => null,
);
if (!empty($matches[2])) {
$matches[2] = trim($matches[2], ', ');
$parser = new PhutilSimpleOptions();
$options = $parser->parse($matches[2]) + $options;
}
$file_name = coalesce($options['name'], $file->getName());
$options['name'] = $file_name;
$is_viewable_image = $file->isViewableImage();
$attrs = array();
if ($is_viewable_image) {
switch ((string)$options['size']) {
case 'full':
$attrs['src'] = $file->getBestURI();
$options['image_class'] = null;
$file_data = $file->getMetadata();
$height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
if ($height) {
$attrs['height'] = $height;
}
$width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
if ($width) {
$attrs['width'] = $width;
}
break;
case 'thumb':
default:
$attrs['src'] = $file->getPreview220URI();
$dimensions =
PhabricatorImageTransformer::getPreviewDimensions($file, 220);
$attrs['width'] = $dimensions['sdx'];
$attrs['height'] = $dimensions['sdy'];
$options['image_class'] = 'phabricator-remarkup-embed-image';
break;
}
}
$bundle['attrs'] = $attrs;
$bundle['options'] = $options;
$bundle['meta'] = array(
'phid' => $file->getPHID(),
'viewable' => $is_viewable_image,
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'name' => $options['name'],
);
$metadata[$phid][] = $bundle;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_EMBED_FILE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
$file_phids = array();
foreach ($metadata as $phid => $bundles) {
foreach ($bundles as $data) {
$options = $data['options'];
$meta = $data['meta'];
if (!$meta['viewable'] || $options['layout'] == 'link') {
$link = id(new PhabricatorFileLinkView())
->setFilePHID($meta['phid'])
->setFileName($meta['name'])
->setFileDownloadURI($meta['dUri'])
->setFileViewURI($meta['uri'])
->setFileViewable($meta['viewable']);
$embed = $link->render();
$engine->overwriteStoredText($data['token'], $embed);
continue;
}
require_celerity_resource('lightbox-attachment-css');
$img = phutil_tag('img', $data['attrs']);
$embed = javelin_tag(
'a',
array(
'href' => $meta['uri'],
'class' => $options['image_class'],
'sigil' => 'lightboxable',
'mustcapture' => true,
'meta' => $meta,
),
$img);
$layout_class = null;
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.
$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
if ($layout_class) {
$embed = phutil_tag(
'div',
array(
'class' => $layout_class,
),
$embed);
}
$engine->overwriteStoredText($data['token'], $embed);
}
$file_phids[] = $phid;
}
$engine->setTextMetadata(self::KEY_EMBED_FILE_PHIDS, $file_phids);
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
index eb048f8c4..006bfda0e 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
@@ -1,62 +1,64 @@
replaceHTML(
'@^([a-zA-Z0-9:_\-]+)$@m',
array($this, 'markupImageMacro'),
$text);
}
public function markupImageMacro($matches) {
if ($this->images === null) {
$this->images = array();
$rows = id(new PhabricatorFileImageMacro())->loadAllWhere(
'isDisabled = 0');
foreach ($rows as $row) {
$this->images[$row->getName()] = $row->getFilePHID();
}
}
- if (array_key_exists($matches[1], $this->images)) {
- $phid = $this->images[$matches[1]];
+ $name = (string)$matches[1];
+
+ if (array_key_exists($name, $this->images)) {
+ $phid = $this->images[$name];
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid);
$style = null;
$src_uri = null;
if ($file) {
$src_uri = $file->getBestURI();
$file_data = $file->getMetadata();
$height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
$width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
if ($height && $width) {
$style = sprintf(
'height: %dpx; width: %dpx;',
$height,
$width
);
}
}
$img = phutil_tag(
'img',
array(
'src' => $src_uri,
'alt' => $matches[1],
'title' => $matches[1],
'style' => $style,
));
return $this->getEngine()->storeText($img);
} else {
return $matches[1];
}
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
index 1254d044a..b569eb60a 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
@@ -1,42 +1,42 @@
replaceHTML(
'@{meme,([^}]+)}$@m',
array($this, 'markupMeme'),
$text);
}
public function markupMeme($matches) {
$options = array(
'src' => null,
'above' => null,
'below' => null,
);
$parser = new PhutilSimpleOptions();
$options = $parser->parse($matches[1]) + $options;
$uri = id(new PhutilURI('/macro/meme/'))
->alter('macro', $options['src'])
->alter('uppertext', $options['above'])
->alter('lowertext', $options['below']);
$img = phutil_tag(
'img',
array(
'src' => (string)$uri,
));
return $this->getEngine()->storeText($img);
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
index f68d05fb6..b2a98fc9a 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
@@ -1,130 +1,130 @@
replaceHTML(
self::REGEX,
array($this, 'markupMention'),
$text);
}
- private function markupMention($matches) {
+ protected function markupMention($matches) {
$engine = $this->getEngine();
$token = $engine->storeText('');
// Store the original text exactly so we can preserve casing if it doesn't
// resolve into a username.
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$original[$token] = $matches[1];
$engine->setTextMetadata($original_key, $original);
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
$username = strtolower($matches[1]);
if (empty($metadata[$username])) {
$metadata[$username] = array();
}
$metadata[$username][] = $token;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata)) {
// No mentions, or we already processed them.
return;
}
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$usernames = array_keys($metadata);
$user_table = new PhabricatorUser();
$real_user_names = queryfx_all(
$user_table->establishConnection('r'),
'SELECT username, phid, realName, isDisabled
FROM %T
WHERE username IN (%Ls)',
$user_table->getTableName(),
$usernames);
$actual_users = array();
$mentioned_key = self::KEY_MENTIONED;
$mentioned = $engine->getTextMetadata($mentioned_key, array());
foreach ($real_user_names as $row) {
$actual_users[strtolower($row['username'])] = $row;
$mentioned[$row['phid']] = $row['phid'];
}
$engine->setTextMetadata($mentioned_key, $mentioned);
foreach ($metadata as $username => $tokens) {
$exists = isset($actual_users[$username]);
if (!$exists) {
$class = 'phabricator-remarkup-mention-unknown';
} else if ($actual_users[$username]['isDisabled']) {
$class = 'phabricator-remarkup-mention-disabled';
} else {
$class = 'phabricator-remarkup-mention-exists';
}
if ($exists) {
$tag = phutil_tag(
'a',
array(
'class' => $class,
'href' => '/p/'.$actual_users[$username]['username'].'/',
'target' => '_blank',
'title' => $actual_users[$username]['realName'],
),
'@'.$actual_users[$username]['username']);
foreach ($tokens as $token) {
$engine->overwriteStoredText($token, $tag);
}
} else {
// NOTE: The structure here is different from the 'exists' branch,
// because we want to preserve the original text capitalization and it
// may differ for each token.
foreach ($tokens as $token) {
$tag = phutil_tag(
'span',
array(
'class' => $class,
),
'@'.idx($original, $token, $username));
$engine->overwriteStoredText($token, $tag);
}
}
}
// Don't re-process these mentions.
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
index 4925459e4..8aa082b62 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
@@ -1,66 +1,66 @@
getObjectNamePrefix();
- return preg_replace_callback(
+ return $this->replaceHTML(
"@\B{{$prefix}(\d+)}\B@",
array($this, 'markupObjectHandle'),
$text);
}
- private function markupObjectHandle($matches) {
+ protected function markupObjectHandle($matches) {
// TODO: These are single gets but should be okay for now, they're behind
// the cache.
$phid = $this->loadObjectPHID($matches[1]);
if (!$phid) {
return $matches[0];
}
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_HANDLE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata[$phid])) {
$metadata[$phid] = array();
}
$metadata[$phid][] = $token;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_HANDLE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata)) {
return;
}
$handles = id(new PhabricatorObjectHandleData(array_keys($metadata)))
->loadHandles();
foreach ($metadata as $phid => $tokens) {
$link = $handles[$phid]->renderLink();
foreach ($tokens as $token) {
$engine->overwriteStoredText($token, $link);
}
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
index b81c37cc2..859bebdc7 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
@@ -1,51 +1,51 @@
getObjectNamePrefix();
$id = $this->getObjectIDPattern();
- return preg_replace_callback(
+ return $this->replaceHTML(
"@\b({$prefix})({$id})(?:#([-\w\d]+))?\b@",
array($this, 'markupObjectNameLink'),
$text);
}
public function markupObjectNameLink($matches) {
list(, $prefix, $id) = $matches;
if (isset($matches[3])) {
$href = $matches[3];
$text = $matches[3];
if (preg_match('@^(?:comment-)?(\d{1,7})$@', $href, $matches)) {
// Maximum length is 7 because 12345678 could be a file hash.
$href = "comment-{$matches[1]}";
$text = $matches[1];
}
$href = "/{$prefix}{$id}#{$href}";
$text = "{$prefix}{$id}#{$text}";
} else {
$href = "/{$prefix}{$id}";
$text = "{$prefix}{$id}";
}
return $this->getEngine()->storeText(
phutil_tag(
'a',
array(
'href' => $href,
),
$text));
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
index 410d507b4..56a84d0d7 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
@@ -1,45 +1,45 @@
replaceHTML(
'@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
array($this, 'markupDocumentLink'),
$text);
}
public function markupDocumentLink($matches) {
$link = trim($matches[1]);
$name = trim(idx($matches, 2, $link));
$name = explode('/', trim($name, '/'));
$name = end($name);
$uri = new PhutilURI($link);
$slug = $uri->getPath();
$fragment = $uri->getFragment();
$slug = PhabricatorSlug::normalize($slug);
$slug = PhrictionDocument::getSlugURI($slug);
$href = (string) id(new PhutilURI($slug))->setFragment($fragment);
if ($this->getEngine()->getState('toc')) {
- $text = phutil_escape_html($name);
+ $text = $name;
} else {
$text = phutil_tag(
'a',
array(
'href' => $href,
'class' => 'phriction-link',
),
$name);
}
return $this->getEngine()->storeText($text);
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
index f4e11e4cc..3b099ef3b 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
@@ -1,43 +1,39 @@
uri = new PhutilURI($text);
if ($this->uri->getDomain() &&
- preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain())) {
+ preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain()) &&
+ idx($this->uri->getQueryParams(), 'v')) {
return $this->markupYoutubeLink();
}
return $text;
}
public function markupYoutubeLink() {
$v = idx($this->uri->getQueryParams(), 'v');
- if ($v) {
- $youtube_src = 'https://www.youtube.com/embed/'.$v;
- $iframe =
- ''.
- phutil_tag(
- 'iframe',
- array(
- 'width' => '650',
- 'height' => '400',
- 'style' => 'margin: 1em auto; border: 0px;',
- 'src' => $youtube_src,
- 'frameborder' => 0,
- ),
- '').
- '
';
- return $this->getEngine()->storeText($iframe);
- } else {
- return $this->uri;
- }
+ $youtube_src = 'https://www.youtube.com/embed/'.$v;
+ $iframe = hsprintf(
+ '%s
',
+ phutil_tag(
+ 'iframe',
+ array(
+ 'width' => '650',
+ 'height' => '400',
+ 'style' => 'margin: 1em auto; border: 0px;',
+ 'src' => $youtube_src,
+ 'frameborder' => 0,
+ ),
+ ''));
+ return $this->getEngine()->storeText($iframe);
}
}