Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2895765
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
78 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/config/custom/PhabricatorCustomLogoConfigType.php b/src/applications/config/custom/PhabricatorCustomLogoConfigType.php
index cc20768119..6811f618cf 100644
--- a/src/applications/config/custom/PhabricatorCustomLogoConfigType.php
+++ b/src/applications/config/custom/PhabricatorCustomLogoConfigType.php
@@ -1,118 +1,157 @@
<?php
final class PhabricatorCustomLogoConfigType
extends PhabricatorConfigOptionType {
public static function getLogoImagePHID() {
$logo = PhabricatorEnv::getEnvConfig('ui.logo');
return idx($logo, 'logoImagePHID');
}
public static function getLogoWordmark() {
$logo = PhabricatorEnv::getEnvConfig('ui.logo');
return idx($logo, 'wordmarkText');
}
+ /**
+ * Return the full URI of the Phorge logo
+ * @param PhabricatorUser Current viewer
+ * @return string Full URI of the Phorge logo
+ */
+ public static function getLogoURI(PhabricatorUser $viewer) {
+ $logo_uri = null;
+
+ $custom_header = self::getLogoImagePHID();
+ if ($custom_header) {
+ $cache = PhabricatorCaches::getImmutableCache();
+ $cache_key_logo = 'ui.custom-header.logo-phid.v3.'.$custom_header;
+ $logo_uri = $cache->getKey($cache_key_logo);
+
+ if (!$logo_uri) {
+ // NOTE: If the file policy has been changed to be restrictive, we'll
+ // miss here and just show the default logo. The cache will fill later
+ // when someone who can see the file loads the page. This might be a
+ // little spooky, see T11982.
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($custom_header))
+ ->execute();
+ $file = head($files);
+ if ($file) {
+ $logo_uri = $file->getViewURI();
+ $cache->setKey($cache_key_logo, $logo_uri);
+ }
+ }
+ }
+
+ if (!$logo_uri) {
+ $logo_uri =
+ celerity_get_resource_uri('/rsrc/image/logo/project-logo.png');
+ }
+
+ return $logo_uri;
+ }
+
public function validateOption(PhabricatorConfigOption $option, $value) {
if (!is_array($value)) {
throw new Exception(
pht(
'Logo configuration is not valid: value must be a dictionary.'));
}
PhutilTypeSpec::checkMap(
$value,
array(
'logoImagePHID' => 'optional string|null',
'wordmarkText' => 'optional string|null',
));
}
public function readRequest(
PhabricatorConfigOption $option,
AphrontRequest $request) {
$viewer = $request->getViewer();
$view_policy = PhabricatorPolicies::POLICY_PUBLIC;
if ($request->getBool('removeLogo')) {
$logo_image_phid = null;
} else if ($request->getFileExists('logoImage')) {
$logo_image = PhabricatorFile::newFromPHPUpload(
idx($_FILES, 'logoImage'),
array(
'name' => 'logo',
'authorPHID' => $viewer->getPHID(),
'viewPolicy' => $view_policy,
'canCDN' => true,
'isExplicitUpload' => true,
));
$logo_image_phid = $logo_image->getPHID();
} else {
$logo_image_phid = self::getLogoImagePHID();
}
$wordmark_text = $request->getStr('wordmarkText');
$value = array(
'logoImagePHID' => $logo_image_phid,
'wordmarkText' => $wordmark_text,
);
$errors = array();
$e_value = null;
try {
$this->validateOption($option, $value);
} catch (Exception $ex) {
$e_value = pht('Invalid');
$errors[] = $ex->getMessage();
$value = array();
}
return array($e_value, $errors, $value, phutil_json_encode($value));
}
public function renderControls(
PhabricatorConfigOption $option,
$display_value,
$e_value) {
try {
$value = phutil_json_decode($display_value);
} catch (Exception $ex) {
$value = array();
}
$logo_image_phid = idx($value, 'logoImagePHID');
$wordmark_text = idx($value, 'wordmarkText');
$controls = array();
// TODO: This should be a PHUIFormFileControl, but that currently only
// works in "workflow" forms. It isn't trivial to convert this form into
// a workflow form, nor is it trivial to make the newer control work
// in non-workflow forms.
$controls[] = id(new AphrontFormFileControl())
->setName('logoImage')
->setLabel(pht('Logo Image'));
if ($logo_image_phid) {
$controls[] = id(new AphrontFormCheckboxControl())
->addCheckbox(
'removeLogo',
1,
pht('Remove Custom Logo'));
}
$controls[] = id(new AphrontFormTextControl())
->setName('wordmarkText')
->setLabel(pht('Wordmark'))
->setPlaceholder(PlatformSymbols::getPlatformServerName())
->setValue($wordmark_text);
return $controls;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index af23126f9b..fe79cd48b3 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,845 +1,912 @@
<?php
final class ManiphestTaskDetailController extends ManiphestController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$task = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->executeOne();
if (!$task) {
return new Aphront404Response();
}
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
$field_list
->setViewer($viewer)
->readFieldsFromStorage($task);
$edit_engine = id(new ManiphestEditEngine())
->setViewer($viewer)
->setTargetObject($task);
$edge_types = array(
ManiphestTaskHasCommitEdgeType::EDGECONST,
ManiphestTaskHasRevisionEdgeType::EDGECONST,
ManiphestTaskHasMockEdgeType::EDGECONST,
PhabricatorObjectMentionedByObjectEdgeType::EDGECONST,
PhabricatorObjectMentionsObjectEdgeType::EDGECONST,
ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST,
);
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes($edge_types);
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$phids = array_keys($phids);
$handles = $viewer->loadHandles($phids);
$timeline = $this->buildTransactionTimeline(
$task,
new ManiphestTransactionQuery());
$monogram = $task->getMonogram();
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($monogram)
->setBorder(true);
$header = $this->buildHeaderView($task);
$details = $this->buildPropertyView($task, $field_list, $edges, $handles);
$description = $this->buildDescriptionView($task);
$curtain = $this->buildCurtain($task, $edit_engine);
$title = pht('%s %s', $monogram, $task->getTitle());
$comment_view = $edit_engine
->buildEditEngineCommentView($task);
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
$related_tabs = array();
$graph_menu = null;
$graph_limit = 200;
$graph_error_message = null;
$task_graph = id(new ManiphestTaskGraph())
->setViewer($viewer)
->setSeedPHID($task->getPHID())
->setLimit($graph_limit)
->loadGraph();
if (!$task_graph->isEmpty()) {
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;
$parent_map = $task_graph->getEdges($parent_type);
$subtask_map = $task_graph->getEdges($subtask_type);
$parent_list = idx($parent_map, $task->getPHID(), array());
$subtask_list = idx($subtask_map, $task->getPHID(), array());
$has_parents = (bool)$parent_list;
$has_subtasks = (bool)$subtask_list;
// First, get a count of direct parent tasks and subtasks. If there
// are too many of these, we just don't draw anything. You can use
// the search button to browse tasks with the search UI instead.
$direct_count = count($parent_list) + count($subtask_list);
$graph_table = null;
if ($direct_count > $graph_limit) {
$graph_error_message = pht(
'This task is directly connected to more than %s other tasks. '.
'Use %s to browse parents or subtasks, or %s to show more of the '.
'graph.',
new PhutilNumber($graph_limit),
phutil_tag('strong', array(), pht('Search...')),
phutil_tag('strong', array(), pht('View Standalone Graph')));
} else {
// If there aren't too many direct tasks, but there are too many total
// tasks, we'll only render directly connected tasks.
if ($task_graph->isOverLimit()) {
$task_graph->setRenderOnlyAdjacentNodes(true);
$graph_error_message = pht(
'This task is connected to more than %s other tasks. '.
'Only direct parents and subtasks are shown here. Use '.
'%s to show more of the graph.',
new PhutilNumber($graph_limit),
phutil_tag('strong', array(), pht('View Standalone Graph')));
}
try {
$graph_table = $task_graph->newGraphTable();
} catch (Throwable $ex) {
phlog($ex);
$graph_error_message = pht(
'There was an unexpected error displaying the task graph. '.
'Use %s to browse parents or subtasks, or %s to show the graph.',
phutil_tag('strong', array(), pht('Search...')),
phutil_tag('strong', array(), pht('View Standalone Graph')));
}
}
if ($graph_error_message) {
$overflow_view = $this->newTaskGraphOverflowView(
$task,
$graph_error_message,
true);
$graph_table = array(
$overflow_view,
$graph_table,
);
}
$graph_menu = $this->newTaskGraphDropdownMenu(
$task,
$has_parents,
$has_subtasks,
true);
$related_tabs[] = id(new PHUITabView())
->setName(pht('Task Graph'))
->setKey('graph')
->appendChild($graph_table);
}
$related_tabs[] = $this->newMocksTab($task, $query);
$related_tabs[] = $this->newMentionsTab($task, $query);
$related_tabs[] = $this->newDuplicatesTab($task, $query);
$tab_view = null;
$related_tabs = array_filter($related_tabs);
if ($related_tabs) {
$tab_group = new PHUITabGroupView();
foreach ($related_tabs as $tab) {
$tab_group->addTab($tab);
}
$related_header = id(new PHUIHeaderView())
->setHeader(pht('Related Objects'));
if ($graph_menu) {
$related_header->addActionLink($graph_menu);
}
$tab_view = id(new PHUIObjectBoxView())
->setHeader($related_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
}
$changes_view = $this->newChangesView($task, $edges);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$changes_view,
$tab_view,
$timeline,
$comment_view,
))
->addPropertySection(pht('Description'), $description)
->addPropertySection(pht('Details'), $details);
-
- return $this->newPage()
+ $page = $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$task->getPHID(),
))
->appendChild($view);
+ if ($this->getIncludeOpenGraphMetadata($viewer, $task)) {
+ $page = $this->addOpenGraphProtocolMetadataTags($page, $task);
+ }
+
+ return $page;
+ }
+
+ /**
+ * Whether the page should include Open Graph metadata tags
+ * @param PhabricatorUser $viewer Viewer of the object
+ * @param object $object
+ * @return bool True if the page should serve Open Graph metadata tags
+ */
+ private function getIncludeOpenGraphMetadata(PhabricatorUser $viewer,
+ $object) {
+ // Don't waste time adding OpenGraph metadata for logged-in users
+ if ($viewer->getIsStandardUser()) {
+ return false;
+ }
+ // Include OpenGraph tags only for public objects
+ return $object->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC;
+ }
+
+ /**
+ * Get Open Graph Protocol metadata values
+ * @param ManiphestTask $task
+ * @return array Map of Open Graph property => value
+ */
+ private function getOpenGraphProtocolMetadataValues($task) {
+ $viewer = $this->getViewer();
+
+ $v = [];
+ $v['og:site_name'] = PlatformSymbols::getPlatformServerName();
+ $v['og:type'] = 'object';
+ $v['og:url'] = PhabricatorEnv::getProductionURI($task->getURI());
+ $v['og:title'] = $task->getMonogram().' '.$task->getTitle();
+
+ $desc = $task->getDescription();
+ if (phutil_nonempty_string($desc)) {
+ $v['og:description'] =
+ PhabricatorMarkupEngine::summarizeSentence($desc);
+ }
+
+ $v['og:image'] =
+ PhabricatorCustomLogoConfigType::getLogoURI($viewer);
+
+ $v['og:image:height'] = 64;
+ $v['og:image:width'] = 64;
+
+ return $v;
+ }
+
+ /**
+ * Add Open Graph Protocol metadata tags to Maniphest task page
+ * @param PhabricatorStandardPageView $page
+ * @param ManiphestTask $task
+ * @return $page with additional OGP <meta> tags
+ */
+ private function addOpenGraphProtocolMetadataTags($page, $task) {
+ foreach ($this->getOpenGraphProtocolMetadataValues($task) as $k => $v) {
+ $page->addHeadItem(phutil_tag(
+ 'meta',
+ array(
+ 'property' => $k,
+ 'content' => $v,
+ )));
+ }
+ return $page;
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle())
->setUser($this->getRequest()->getUser())
->setPolicyObject($task);
$priority_name = ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority());
$priority_color = ManiphestTaskPriority::getTaskPriorityColor(
$task->getPriority());
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription(
$status, $priority_name);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
$view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon(
$task->getStatus()).' '.$priority_color);
if (ManiphestTaskPoints::getIsEnabled()) {
$points = $task->getPoints();
if ($points !== null) {
$points_name = pht('%s %s',
$task->getPoints(),
ManiphestTaskPoints::getPointsLabel());
$tag = id(new PHUITagView())
->setName($points_name)
->setColor(PHUITagView::COLOR_BLUE)
->setType(PHUITagView::TYPE_SHADE);
$view->addTag($tag);
}
}
$subtype = $task->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$view->addTag($subtype_tag);
}
return $view;
}
private function buildCurtain(
ManiphestTask $task,
PhabricatorEditEngine $edit_engine) {
$viewer = $this->getViewer();
$id = $task->getID();
$phid = $task->getPHID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$task,
PhabricatorPolicyCapability::CAN_EDIT);
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task);
// We expect a policy dialog if you can't edit the task, and expect a
// lock override dialog if you can't interact with it.
$workflow_edit = (!$can_edit || !$can_interact);
$curtain = $this->newCurtainView($task);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("/task/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow($workflow_edit));
$subtype_map = $task->newEditEngineSubtypeMap();
$subtask_options = $subtype_map->getCreateFormsForSubtype(
$edit_engine,
$task);
// If no forms are available, we want to show the user an error.
// If one form is available, we take them user directly to the form.
// If two or more forms are available, we give the user a choice.
// The "subtask" controller handles the first case (no forms) and the
// third case (more than one form). In the case of one form, we link
// directly to the form.
$subtask_uri = "/task/subtask/{$id}/";
$subtask_workflow = true;
if (count($subtask_options) == 1) {
$subtask_form = head($subtask_options);
$form_key = $subtask_form->getIdentifier();
$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))
->replaceQueryParam('parent', $id)
->replaceQueryParam('template', $id)
->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus());
$subtask_workflow = false;
}
$subtask_uri = $this->getApplicationURI($subtask_uri);
$subtask_item = id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($subtask_uri)
->setIcon('fa-level-down')
->setDisabled(!$subtask_options)
->setWorkflow($subtask_workflow);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$task);
$submenu_actions = array(
$subtask_item,
ManiphestTaskHasParentRelationship::RELATIONSHIPKEY,
ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY,
ManiphestTaskMergeInRelationship::RELATIONSHIPKEY,
ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY,
);
$task_submenu = $relationship_list->newActionSubmenu($submenu_actions)
->setName(pht('Edit Related Tasks...'))
->setIcon('fa-anchor');
$curtain->addAction($task_submenu);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
$viewer_phid = $viewer->getPHID();
$owner_phid = $task->getOwnerPHID();
$author_phid = $task->getAuthorPHID();
$handles = $viewer->loadHandles(array($owner_phid, $author_phid));
$assigned_refs = id(new PHUICurtainObjectRefListView())
->setViewer($viewer)
->setEmptyMessage(pht('None'));
if ($owner_phid) {
$assigned_ref = $assigned_refs->newObjectRefView()
->setHandle($handles[$owner_phid])
->setHighlighted($owner_phid === $viewer_phid);
}
$curtain->newPanel()
->setHeaderText(pht('Assigned To'))
->appendChild($assigned_refs);
$author_refs = id(new PHUICurtainObjectRefListView())
->setViewer($viewer);
$author_ref = $author_refs->newObjectRefView()
->setHandle($handles[$author_phid])
->setEpoch($task->getDateCreated())
->setHighlighted($author_phid === $viewer_phid);
$curtain->newPanel()
->setHeaderText(pht('Authored By'))
->appendChild($author_refs);
return $curtain;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
$handles) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject,
),
$source));
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
if ($view->hasAnyProperties()) {
return $view;
}
return null;
}
private function buildDescriptionView(ManiphestTask $task) {
$viewer = $this->getViewer();
$section = null;
$description = $task->getDescription();
if (strlen($description)) {
$section = new PHUIPropertyListView();
$section->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
id(new PHUIRemarkupView($viewer, $description))
->setContextObject($task)));
}
return $section;
}
private function newMocksTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$mock_type = ManiphestTaskHasMockEdgeType::EDGECONST;
$mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type));
if (!$mock_phids) {
return null;
}
$viewer = $this->getViewer();
$handles = $viewer->loadHandles($mock_phids);
// TODO: It would be nice to render this as pinboard-style thumbnails,
// similar to "{M123}", instead of a list of links.
$view = id(new PHUIPropertyListView())
->addProperty(pht('Mocks'), $handles->renderList());
return id(new PHUITabView())
->setName(pht('Mocks'))
->setKey('mocks')
->appendChild($view);
}
private function newMentionsTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST;
$out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type));
// Filter out any mentioned users from the list. These are not generally
// very interesting to show in a relationship summary since they usually
// end up as subscribers anyway.
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($out_phids as $key => $out_phid) {
if (phid_get_type($out_phid) == $user_type) {
unset($out_phids[$key]);
}
}
if (!$in_phids && !$out_phids) {
return null;
}
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$out_handles = $viewer->loadHandles($out_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$out_handles = $this->getCompleteHandles($out_handles);
if (!count($in_handles) && !count($out_handles)) {
return null;
}
$view = new PHUIPropertyListView();
if (count($in_handles)) {
$view->addProperty(pht('Mentioned In'), $in_handles->renderList());
}
if (count($out_handles)) {
$view->addProperty(pht('Mentioned Here'), $out_handles->renderList());
}
return id(new PHUITabView())
->setName(pht('Mentions'))
->setKey('mentions')
->appendChild($view);
}
private function newDuplicatesTab(
ManiphestTask $task,
PhabricatorEdgeQuery $edge_query) {
$in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;
$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));
$viewer = $this->getViewer();
$in_handles = $viewer->loadHandles($in_phids);
$in_handles = $this->getCompleteHandles($in_handles);
$view = new PHUIPropertyListView();
if (!count($in_handles)) {
return null;
}
$view->addProperty(
pht('Duplicates Merged Here'), $in_handles->renderList());
return id(new PHUITabView())
->setName(pht('Duplicates'))
->setKey('duplicates')
->appendChild($view);
}
private function getCompleteHandles(PhabricatorHandleList $handles) {
$phids = array();
foreach ($handles as $phid => $handle) {
if (!$handle->isComplete()) {
continue;
}
$phids[] = $phid;
}
return $handles->newSublist($phids);
}
private function newChangesView(ManiphestTask $task, array $edges) {
$viewer = $this->getViewer();
$revision_type = ManiphestTaskHasRevisionEdgeType::EDGECONST;
$commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST;
$revision_phids = idx($edges, $revision_type, array());
$revision_phids = array_keys($revision_phids);
$revision_phids = array_fuse($revision_phids);
$commit_phids = idx($edges, $commit_type, array());
$commit_phids = array_keys($commit_phids);
$commit_phids = array_fuse($commit_phids);
if (!$revision_phids && !$commit_phids) {
return null;
}
if ($commit_phids) {
$link_type = DiffusionCommitHasRevisionEdgeType::EDGECONST;
$link_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($link_type));
$link_query->execute();
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withPHIDs($commit_phids)
->execute();
$commits = mpull($commits, null, 'getPHID');
} else {
$commits = array();
}
if ($revision_phids) {
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withPHIDs($revision_phids)
->execute();
$revisions = mpull($revisions, null, 'getPHID');
} else {
$revisions = array();
}
$handle_phids = array();
$any_linked = false;
$any_status = false;
$idx = 0;
$objects = array();
foreach ($commit_phids as $commit_phid) {
$handle_phids[] = $commit_phid;
$link_phids = $link_query->getDestinationPHIDs(array($commit_phid));
foreach ($link_phids as $link_phid) {
$handle_phids[] = $link_phid;
unset($revision_phids[$link_phid]);
$any_linked = true;
}
$commit = idx($commits, $commit_phid);
if ($commit) {
$repository_phid = $commit->getRepository()->getPHID();
$handle_phids[] = $repository_phid;
} else {
$repository_phid = null;
}
$status_view = null;
if ($commit) {
$status = $commit->getAuditStatusObject();
if (!$status->isNoAudit()) {
$status_view = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setIcon($status->getIcon())
->setColor($status->getColor())
->setName($status->getName());
}
}
$object_link = null;
if ($commit) {
$commit_monogram = $commit->getDisplayName();
$commit_monogram = phutil_tag(
'span',
array(
'class' => 'object-name',
),
$commit_monogram);
$commit_link = javelin_tag(
'a',
array(
'href' => $commit->getURI(),
'sigil' => 'hovercard',
'meta' => array(
'hovercardSpec' => array(
'objectPHID' => $commit->getPHID(),
),
),
),
$commit->getSummary());
$object_link = array(
$commit_monogram,
' ',
$commit_link,
);
}
$objects[] = array(
'objectPHID' => $commit_phid,
'objectLink' => $object_link,
'repositoryPHID' => $repository_phid,
'revisionPHIDs' => $link_phids,
'status' => $status_view,
'order' => id(new PhutilSortVector())
->addInt($repository_phid ? 1 : 0)
->addString((string)$repository_phid)
->addInt(1)
->addInt($idx++),
);
}
foreach ($revision_phids as $revision_phid) {
$handle_phids[] = $revision_phid;
$revision = idx($revisions, $revision_phid);
if ($revision) {
$repository_phid = $revision->getRepositoryPHID();
$handle_phids[] = $repository_phid;
} else {
$repository_phid = null;
}
if ($revision) {
$icon = $revision->getStatusIcon();
$color = $revision->getStatusIconColor();
$name = $revision->getStatusDisplayName();
$status_view = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setIcon($icon)
->setColor($color)
->setName($name);
} else {
$status_view = null;
}
$object_link = null;
if ($revision) {
$revision_monogram = $revision->getMonogram();
$revision_monogram = phutil_tag(
'span',
array(
'class' => 'object-name',
),
$revision_monogram);
$revision_link = javelin_tag(
'a',
array(
'href' => $revision->getURI(),
'sigil' => 'hovercard',
'meta' => array(
'hovercardSpec' => array(
'objectPHID' => $revision->getPHID(),
),
),
),
$revision->getTitle());
$object_link = array(
$revision_monogram,
' ',
$revision_link,
);
}
$objects[] = array(
'objectPHID' => $revision_phid,
'objectLink' => $object_link,
'repositoryPHID' => $repository_phid,
'revisionPHIDs' => array(),
'status' => $status_view,
'order' => id(new PhutilSortVector())
->addInt($repository_phid ? 1 : 0)
->addString((string)$repository_phid)
->addInt(0)
->addInt($idx++),
);
}
$handles = $viewer->loadHandles($handle_phids);
$order = ipull($objects, 'order');
$order = msortv($order, 'getSelf');
$objects = array_select_keys($objects, array_keys($order));
$last_repository = false;
$rows = array();
$rowd = array();
foreach ($objects as $object) {
$repository_phid = $object['repositoryPHID'];
if ($repository_phid !== $last_repository) {
$repository_link = null;
if ($repository_phid) {
$repository_handle = $handles[$repository_phid];
$rows[] = array(
$repository_handle->renderLink(),
);
$rowd[] = true;
}
$last_repository = $repository_phid;
}
$object_phid = $object['objectPHID'];
$handle = $handles[$object_phid];
$object_link = $object['objectLink'];
if ($object_link === null) {
$object_link = $handle->renderLink();
}
$object_icon = id(new PHUIIconView())
->setIcon($handle->getIcon());
$status_view = $object['status'];
if ($status_view) {
$any_status = true;
}
$revision_tags = array();
foreach ($object['revisionPHIDs'] as $link_phid) {
$revision_handle = $handles[$link_phid];
$revision_name = $revision_handle->getName();
$revision_tags[] = $revision_handle
->renderHovercardLink($revision_name);
}
$revision_tags = phutil_implode_html(
phutil_tag('br'),
$revision_tags);
$rowd[] = false;
$rows[] = array(
$object_icon,
$status_view,
$revision_tags,
$object_link,
);
}
$changes_table = id(new AphrontTableView($rows))
->setNoDataString(pht('This task has no related commits or revisions.'))
->setRowDividers($rowd)
->setColumnClasses(
array(
'indent center',
null,
null,
'wide pri object-link',
))
->setColumnVisibility(
array(
true,
$any_status,
$any_linked,
true,
))
->setDeviceVisibility(
array(
false,
$any_status,
false,
true,
));
$changes_header = id(new PHUIHeaderView())
->setHeader(pht('Revisions and Commits'));
$changes_view = id(new PHUIObjectBoxView())
->setHeader($changes_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($changes_table);
return $changes_view;
}
}
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
index 005ede23a7..eb7b775f0f 100644
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -1,929 +1,928 @@
<?php
/**
* This is a standard Phabricator page with menus, Javelin, DarkConsole, and
* basic styles.
*/
final class PhabricatorStandardPageView extends PhabricatorBarePageView
implements AphrontResponseProducerInterface {
private $baseURI;
private $applicationName;
private $glyph;
private $menuContent;
private $showChrome = true;
private $classes = array();
private $disableConsole;
private $pageObjects = array();
private $applicationMenu;
private $showFooter = true;
private $showDurableColumn = true;
private $quicksandConfig = array();
private $tabs;
private $crumbs;
private $navigation;
private $footer;
private $headItems = array();
public function setShowFooter($show_footer) {
$this->showFooter = $show_footer;
return $this;
}
public function getShowFooter() {
return $this->showFooter;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function setPageObjectPHIDs(array $phids) {
$this->pageObjects = $phids;
return $this;
}
public function setShowDurableColumn($show) {
$this->showDurableColumn = $show;
return $this;
}
public function getShowDurableColumn() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
return false;
}
$conpherence_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorConpherenceApplication',
$viewer);
if (!$conpherence_installed) {
return false;
}
if ($this->isQuicksandBlacklistURI()) {
return false;
}
return true;
}
private function isQuicksandBlacklistURI() {
$request = $this->getRequest();
if (!$request) {
return false;
}
$patterns = $this->getQuicksandURIPatternBlacklist();
$path = $request->getRequestURI()->getPath();
foreach ($patterns as $pattern) {
if (preg_match('(^'.$pattern.'$)', $path)) {
return true;
}
}
return false;
}
public function getDurableColumnVisible() {
$column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY;
return (bool)$this->getUserPreference($column_key, false);
}
public function getDurableColumnMinimize() {
$column_key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY;
return (bool)$this->getUserPreference($column_key, false);
}
public function addQuicksandConfig(array $config) {
$this->quicksandConfig = $config + $this->quicksandConfig;
return $this;
}
public function getQuicksandConfig() {
return $this->quicksandConfig;
}
public function setCrumbs(PHUICrumbsView $crumbs) {
$this->crumbs = $crumbs;
return $this;
}
public function getCrumbs() {
return $this->crumbs;
}
public function setTabs(PHUIListView $tabs) {
$tabs->setType(PHUIListView::TABBAR_LIST);
$tabs->addClass('phabricator-standard-page-tabs');
$this->tabs = $tabs;
return $this;
}
public function getTabs() {
return $this->tabs;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
public function getTitle() {
$glyph_key = PhabricatorTitleGlyphsSetting::SETTINGKEY;
$glyph_on = PhabricatorTitleGlyphsSetting::VALUE_TITLE_GLYPHS;
$glyph_setting = $this->getUserPreference($glyph_key, $glyph_on);
$use_glyph = ($glyph_setting == $glyph_on);
$title = parent::getTitle();
$prefix = null;
if ($use_glyph) {
$prefix = $this->getGlyph();
} else {
$application_name = $this->getApplicationName();
if (strlen($application_name)) {
$prefix = '['.$application_name.']';
}
}
if (phutil_nonempty_string($prefix)) {
$title = $prefix.' '.$title;
}
return $title;
}
protected function willRenderPage() {
$footer = $this->renderFooter();
// NOTE: A cleaner solution would be to let body layout elements implement
// some kind of "LayoutInterface" so content can be embedded inside frames,
// but there's only really one use case for this for now.
$children = $this->renderChildren();
if ($children) {
$layout = head($children);
if ($layout instanceof PHUIFormationView) {
$layout->setFooter($footer);
$footer = null;
}
}
$this->footer = $footer;
parent::willRenderPage();
if (!$this->getRequest()) {
throw new Exception(
pht(
'You must set the %s to render a %s.',
'Request',
__CLASS__));
}
$console = $this->getConsole();
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-zindex-css');
require_celerity_resource('phui-button-css');
require_celerity_resource('phui-spacing-css');
require_celerity_resource('phui-form-css');
require_celerity_resource('phabricator-standard-page-view');
require_celerity_resource('conpherence-durable-column-view');
require_celerity_resource('font-lato');
Javelin::initBehavior('workflow', array());
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
}
if ($user) {
if ($user->isUserActivated()) {
$offset = $user->getTimeZoneOffset();
$ignore_key = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY;
$ignore = $user->getUserSetting($ignore_key);
Javelin::initBehavior(
'detect-timezone',
array(
'offset' => $offset,
'uri' => '/settings/timezone/',
'message' => pht(
'Your browser timezone setting differs from the timezone '.
'setting in your profile, click to reconcile.'),
'ignoreKey' => $ignore_key,
'ignore' => $ignore,
));
if ($user->getIsAdmin()) {
$server_https = $request->isHTTPS();
$server_protocol = $server_https ? 'HTTPS' : 'HTTP';
$client_protocol = $server_https ? 'HTTP' : 'HTTPS';
$doc_name = 'Configuring a Preamble Script';
$doc_href = PhabricatorEnv::getDoclink($doc_name);
Javelin::initBehavior(
'setup-check-https',
array(
'server_https' => $server_https,
'doc_name' => pht('See Documentation'),
'doc_href' => $doc_href,
'message' => pht(
'This server thinks you are using %s, but your '.
'client is convinced that it is using %s. This is a serious '.
'misconfiguration with subtle, but significant, consequences.',
$server_protocol, $client_protocol),
));
}
}
Javelin::initBehavior('lightbox-attachments');
}
Javelin::initBehavior('aphront-form-disable-on-submit');
Javelin::initBehavior('toggle-class', array());
Javelin::initBehavior('history-install');
Javelin::initBehavior('phabricator-gesture');
$current_token = null;
if ($user) {
$current_token = $user->getCSRFToken();
}
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'viaHeader' => AphrontRequest::getViaHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior('device');
Javelin::initBehavior(
'high-security-warning',
$this->getHighSecurityWarningConfig());
if (PhabricatorEnv::isReadOnly()) {
Javelin::initBehavior(
'read-only-warning',
array(
'message' => PhabricatorEnv::getReadOnlyMessage(),
'uri' => PhabricatorEnv::getReadOnlyURI(),
));
}
// If we aren't showing the page chrome, skip rendering DarkConsole and the
// main menu, since they won't be visible on the page.
if (!$this->getShowChrome()) {
return;
}
if ($console) {
require_celerity_resource('aphront-dark-console-css');
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
Javelin::initBehavior(
'dark-console',
$this->getConsoleConfig());
}
if ($user) {
$viewer = $user;
} else {
$viewer = new PhabricatorUser();
}
$menu = id(new PhabricatorMainMenuView())
->setUser($viewer);
if ($this->getController()) {
$menu->setController($this->getController());
}
$application_menu = $this->applicationMenu;
if ($application_menu) {
if ($application_menu instanceof PHUIApplicationMenuView) {
$crumbs = $this->getCrumbs();
if ($crumbs) {
$application_menu->setCrumbs($crumbs);
}
$application_menu = $application_menu->buildListView();
}
$menu->setApplicationMenu($application_menu);
}
$this->menuContent = $menu->render();
}
/**
* Insert a HTML element into <head> of the page to render.
- * Used by PhameBlogViewController.
*
* @param PhutilSafeHTML HTML header to add
*/
public function addHeadItem($html) {
if ($html instanceof PhutilSafeHTML) {
$this->headItems[] = $html;
}
}
protected function getHead() {
$monospaced = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$monospaced = $user->getUserSetting(
PhabricatorMonospacedFontSetting::SETTINGKEY);
}
}
$response = CelerityAPI::getStaticResourceResponse();
$font_css = null;
if (!empty($monospaced)) {
// We can't print this normally because escaping quotation marks will
// break the CSS. Instead, filter it strictly and then mark it as safe.
$monospaced = new PhutilSafeHTML(
PhabricatorMonospacedFontSetting::filterMonospacedCSSRule(
$monospaced));
$font_css = hsprintf(
'<style type="text/css">'.
'.PhabricatorMonospaced, '.
'.phabricator-remarkup .remarkup-code-block .remarkup-code, '.
'.phabricator-remarkup .remarkup-monospaced '.
'{ font: %s !important; } '.
'</style>',
$monospaced);
}
return hsprintf(
'%s%s%s%s',
parent::getHead(),
$font_css,
phutil_implode_html('', $this->headItems),
$response->renderSingleResource('javelin-magical-init', 'phabricator'));
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function willSendResponse($response) {
$request = $this->getRequest();
$response = parent::willSendResponse($response);
$console = $request->getApplicationConfiguration()->getConsole();
if ($console) {
$response = PhutilSafeHTML::applyFunction(
'str_replace',
hsprintf('<darkconsole />'),
$console->render($request),
$response);
}
return $response;
}
protected function getBody() {
$user = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
}
$header_chrome = null;
if ($this->getShowChrome()) {
$header_chrome = $this->menuContent;
}
$classes = array();
$classes[] = 'main-page-frame';
$developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&
DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning = phutil_tag_div(
'aphront-developer-error-callout',
pht(
'This page raised PHP errors. Find them in DarkConsole '.
'or the error log.'));
}
$main_page = phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page',
'class' => 'phabricator-standard-page',
),
array(
$developer_warning,
$header_chrome,
phutil_tag(
'div',
array(
'id' => 'phabricator-standard-page-body',
'class' => 'phabricator-standard-page-body',
),
$this->renderPageBodyContent()),
));
$durable_column = null;
if ($this->getShowDurableColumn()) {
$is_visible = $this->getDurableColumnVisible();
$is_minimize = $this->getDurableColumnMinimize();
$durable_column = id(new ConpherenceDurableColumnView())
->setSelectedConpherence(null)
->setUser($user)
->setQuicksandConfig($this->buildQuicksandConfig())
->setVisible($is_visible)
->setMinimize($is_minimize)
->setInitialLoad(true);
if ($is_minimize) {
$this->classes[] = 'minimize-column';
}
}
Javelin::initBehavior('quicksand-blacklist', array(
'patterns' => $this->getQuicksandURIPatternBlacklist(),
));
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => 'main-page-frame',
),
array(
$main_page,
$durable_column,
));
}
private function renderPageBodyContent() {
$console = $this->getConsole();
$body = parent::getBody();
$nav = $this->getNavigation();
$tabs = $this->getTabs();
if ($nav) {
$crumbs = $this->getCrumbs();
if ($crumbs) {
$nav->setCrumbs($crumbs);
}
$nav->appendChild($body);
$nav->appendFooter($this->footer);
$content = phutil_implode_html('', array($nav->render()));
} else {
$content = array();
$crumbs = $this->getCrumbs();
if ($crumbs) {
if ($this->getTabs()) {
$crumbs->setBorder(true);
}
$content[] = $crumbs;
}
$tabs = $this->getTabs();
if ($tabs) {
$content[] = $tabs;
}
$content[] = $body;
$content[] = $this->footer;
$content = phutil_implode_html('', $content);
}
return array(
($console ? hsprintf('<darkconsole />') : null),
$content,
);
}
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
$tail = array(
parent::getTail(),
);
$response = CelerityAPI::getStaticResourceResponse();
if ($request->isHTTPS()) {
$with_protocol = 'https';
} else {
$with_protocol = 'http';
}
$servers = PhabricatorNotificationServerRef::getEnabledClientServers(
$with_protocol);
if ($servers) {
if ($user && $user->isLoggedIn()) {
// TODO: We could tell the browser about all the servers and let it
// do random reconnects to improve reliability.
shuffle($servers);
$server = head($servers);
$client_uri = $server->getWebsocketURI();
Javelin::initBehavior(
'aphlict-listen',
array(
'websocketURI' => (string)$client_uri,
) + $this->buildAphlictListenConfigData());
CelerityAPI::getStaticResourceResponse()
->addContentSecurityPolicyURI('connect-src', $client_uri);
}
}
$tail[] = $response->renderHTMLFooter($this->getFrameable());
return $tail;
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
$agent = AphrontRequest::getHTTPHeader('User-Agent');
// Try to guess the device resolution based on UA strings to avoid a flash
// of incorrectly-styled content.
$device_guess = 'device-desktop';
if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
$device_guess = 'device-phone device';
} else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
$device_guess = 'device-tablet device';
}
$classes[] = $device_guess;
if (preg_match('@Windows@', $agent)) {
$classes[] = 'platform-windows';
} else if (preg_match('@Macintosh@', $agent)) {
$classes[] = 'platform-mac';
} else if (preg_match('@X11@', $agent)) {
$classes[] = 'platform-linux';
}
if ($this->getRequest()->getStr('__print__')) {
$classes[] = 'printable';
}
if ($this->getRequest()->getStr('__aural__')) {
$classes[] = 'audible';
}
$classes[] = 'phui-theme-'.PhabricatorEnv::getEnvConfig('ui.header-color');
foreach ($this->classes as $class) {
$classes[] = $class;
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
private function getConsoleConfig() {
$user = $this->getRequest()->getUser();
$headers = array();
if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {
$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';
}
if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {
$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;
}
if ($user) {
$setting_tab = PhabricatorDarkConsoleTabSetting::SETTINGKEY;
$setting_visible = PhabricatorDarkConsoleVisibleSetting::SETTINGKEY;
$tab = $user->getUserSetting($setting_tab);
$visible = $user->getUserSetting($setting_visible);
} else {
$tab = null;
$visible = true;
}
return array(
// NOTE: We use a generic label here to prevent input reflection
// and mitigate compression attacks like BREACH. See discussion in
// T3684.
'uri' => pht('Main Request'),
'selected' => $tab,
'visible' => $visible,
'headers' => $headers,
);
}
private function getHighSecurityWarningConfig() {
$user = $this->getRequest()->getUser();
$show = false;
if ($user->hasSession()) {
$hisec = ($user->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$show = true;
}
}
return array(
'show' => $show,
'uri' => '/auth/session/downgrade/',
'message' => pht(
'Your session is in high security mode. When you '.
'finish using it, click here to leave.'),
);
}
private function renderFooter() {
if (!$this->getShowChrome()) {
return null;
}
if (!$this->getShowFooter()) {
return null;
}
$items = PhabricatorEnv::getEnvConfig('ui.footer-items');
if (!$items) {
return null;
}
$foot = array();
foreach ($items as $item) {
$name = idx($item, 'name', pht('Unnamed Footer Item'));
$href = idx($item, 'href');
if (!PhabricatorEnv::isValidURIForLink($href)) {
$href = null;
}
if ($href !== null) {
$tag = 'a';
} else {
$tag = 'span';
}
$foot[] = phutil_tag(
$tag,
array(
'href' => $href,
),
$name);
}
$foot = phutil_implode_html(" \xC2\xB7 ", $foot);
return phutil_tag(
'div',
array(
'class' => 'phabricator-standard-page-footer grouped',
),
$foot);
}
public function renderForQuicksand() {
parent::willRenderPage();
$response = $this->renderPageBodyContent();
$response = $this->willSendResponse($response);
$extra_config = $this->getQuicksandConfig();
return array(
'content' => hsprintf('%s', $response),
) + $this->buildQuicksandConfig()
+ $extra_config;
}
private function buildQuicksandConfig() {
$viewer = $this->getRequest()->getUser();
$controller = $this->getController();
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($viewer);
$dropdown_query->execute();
$hisec_warning_config = $this->getHighSecurityWarningConfig();
$console_config = null;
$console = $this->getConsole();
if ($console) {
$console_config = $this->getConsoleConfig();
}
$upload_enabled = false;
if ($controller) {
$upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();
}
$application_class = null;
$application_search_icon = null;
$application_help = null;
$controller = $this->getController();
if ($controller) {
$application = $controller->getCurrentApplication();
if ($application) {
$application_class = get_class($application);
if ($application->getApplicationSearchDocumentTypes()) {
$application_search_icon = $application->getIcon();
}
$help_items = $application->getHelpMenuItems($viewer);
if ($help_items) {
$help_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($help_items as $help_item) {
$help_list->addAction($help_item);
}
$application_help = $help_list->getDropdownMenuMetadata();
}
}
}
return array(
'title' => $this->getTitle(),
'bodyClasses' => $this->getBodyClasses(),
'aphlictDropdownData' => array(
$dropdown_query->getNotificationData(),
$dropdown_query->getConpherenceData(),
),
'globalDragAndDrop' => $upload_enabled,
'hisecWarningConfig' => $hisec_warning_config,
'consoleConfig' => $console_config,
'applicationClass' => $application_class,
'applicationSearchIcon' => $application_search_icon,
'helpItems' => $application_help,
) + $this->buildAphlictListenConfigData();
}
private function buildAphlictListenConfigData() {
$user = $this->getRequest()->getUser();
$subscriptions = $this->pageObjects;
$subscriptions[] = $user->getPHID();
return array(
'pageObjects' => array_fill_keys($this->pageObjects, true),
'subscriptions' => $subscriptions,
);
}
private function getQuicksandURIPatternBlacklist() {
$applications = PhabricatorApplication::getAllApplications();
$blacklist = array();
foreach ($applications as $application) {
$blacklist[] = $application->getQuicksandURIPatternBlacklist();
}
// See T4340. Currently, Phortune and Auth both require pulling in external
// Javascript (for Stripe card management and Recaptcha, respectively).
// This can put us in a position where the user loads a page with a
// restrictive Content-Security-Policy, then uses Quicksand to navigate to
// a page which needs to load external scripts. For now, just blacklist
// these entire applications since we aren't giving up anything
// significant by doing so.
$blacklist[] = array(
'/phortune/.*',
'/auth/.*',
);
return array_mergev($blacklist);
}
private function getUserPreference($key, $default = null) {
$request = $this->getRequest();
if (!$request) {
return $default;
}
$user = $request->getUser();
if (!$user) {
return $default;
}
return $user->getUserSetting($key);
}
public function produceAphrontResponse() {
$controller = $this->getController();
$viewer = $this->getUser();
if ($viewer && $viewer->getPHID()) {
$object_phids = $this->pageObjects;
foreach ($object_phids as $object_phid) {
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$viewer,
$object_phid);
}
}
if ($this->getRequest()->isQuicksand()) {
$content = $this->renderForQuicksand();
$response = id(new AphrontAjaxResponse())
->setContent($content);
} else {
// See T13247. Try to find some navigational menu items to create a
// mobile navigation menu from.
$application_menu = $controller->buildApplicationMenu();
if (!$application_menu) {
$navigation = $this->getNavigation();
if ($navigation) {
$application_menu = $navigation->getMenu();
}
}
$this->applicationMenu = $application_menu;
$content = $this->render();
$response = id(new AphrontWebpageResponse())
->setContent($content)
->setFrameable($this->getFrameable());
}
return $response;
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php
index f0b2bbbe83..8db27628d9 100644
--- a/src/view/page/menu/PhabricatorMainMenuView.php
+++ b/src/view/page/menu/PhabricatorMainMenuView.php
@@ -1,736 +1,714 @@
<?php
final class PhabricatorMainMenuView extends AphrontView {
private $controller;
private $applicationMenu;
public function setApplicationMenu(PHUIListView $application_menu) {
$this->applicationMenu = $application_menu;
return $this;
}
public function getApplicationMenu() {
return $this->applicationMenu;
}
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
private static function getFavicons() {
$refs = array();
$refs['favicon'] = id(new PhabricatorFaviconRef())
->setWidth(64)
->setHeight(64);
$refs['message_favicon'] = id(new PhabricatorFaviconRef())
->setWidth(64)
->setHeight(64)
->setEmblems(
array(
'dot-pink',
null,
null,
null,
));
id(new PhabricatorFaviconRefQuery())
->withRefs($refs)
->execute();
return mpull($refs, 'getURI');
}
public function render() {
$viewer = $this->getViewer();
require_celerity_resource('phabricator-main-menu-view');
$header_id = celerity_generate_unique_node_id();
$menu_bar = array();
$alerts = array();
$search_button = '';
$app_button = '';
$aural = null;
$is_full = $this->isFullSession($viewer);
if ($is_full) {
list($menu, $dropdowns, $aural) = $this->renderNotificationMenu();
if (array_filter($menu)) {
$alerts[] = $menu;
}
$menu_bar = array_merge($menu_bar, $dropdowns);
$app_button = $this->renderApplicationMenuButton();
$search_button = $this->renderSearchMenuButton($header_id);
} else if (!$viewer->isLoggedIn()) {
$app_button = $this->renderApplicationMenuButton();
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$search_button = $this->renderSearchMenuButton($header_id);
}
}
if ($search_button) {
$search_menu = $this->renderPhabricatorSearchMenu();
} else {
$search_menu = null;
}
if ($alerts) {
$alerts = javelin_tag(
'div',
array(
'class' => 'phabricator-main-menu-alerts',
'aural' => false,
),
$alerts);
}
if ($aural) {
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
phutil_implode_html(' ', $aural));
}
$extensions = PhabricatorMainMenuBarExtension::getAllEnabledExtensions();
foreach ($extensions as $extension) {
$extension
->setViewer($viewer)
->setIsFullSession($is_full);
$controller = $this->getController();
if ($controller) {
$extension->setController($controller);
$application = $controller->getCurrentApplication();
if ($application) {
$extension->setApplication($application);
}
}
}
if (!$is_full) {
foreach ($extensions as $key => $extension) {
if ($extension->shouldRequireFullSession()) {
unset($extensions[$key]);
}
}
}
foreach ($extensions as $key => $extension) {
if (!$extension->isExtensionEnabledForViewer($extension->getViewer())) {
unset($extensions[$key]);
}
}
$menus = array();
foreach ($extensions as $extension) {
foreach ($extension->buildMainMenus() as $menu) {
$menus[] = $menu;
}
}
// Because we display these with "float: right", reverse their order before
// rendering them into the document so that the extension order and display
// order are the same.
$menus = array_reverse($menus);
foreach ($menus as $menu) {
$menu_bar[] = $menu;
}
$classes = array();
$classes[] = 'phabricator-main-menu';
$classes[] = 'phabricator-main-menu-background';
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => $header_id,
),
array(
$app_button,
$search_button,
$this->renderPhabricatorLogo(),
$alerts,
$aural,
$search_menu,
$menu_bar,
));
}
private function renderSearch() {
$viewer = $this->getViewer();
$result = null;
$keyboard_config = array(
'helpURI' => '/help/keyboardshortcut/',
);
if ($viewer->isLoggedIn()) {
$show_search = $viewer->isUserActivated();
} else {
$show_search = PhabricatorEnv::getEnvConfig('policy.allow-public');
}
if ($show_search) {
$search = new PhabricatorMainMenuSearchView();
$search->setViewer($viewer);
$application = null;
$controller = $this->getController();
if ($controller) {
$application = $controller->getCurrentApplication();
}
if ($application) {
$search->setApplication($application);
}
$result = $search;
$keyboard_config['searchID'] = $search->getID();
}
$keyboard_config['pht'] = array(
'/' => pht('Give keyboard focus to the search box.'),
'?' => pht('Show keyboard shortcut help for the current page.'),
);
Javelin::initBehavior(
'phabricator-keyboard-shortcuts',
$keyboard_config);
if ($result) {
$result = id(new PHUIListItemView())
->addClass('phabricator-main-menu-search')
->appendChild($result);
}
return $result;
}
public function renderApplicationMenuButton() {
$dropdown = $this->renderApplicationMenu();
if (!$dropdown) {
return null;
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setIcon('fa-bars')
->addClass('phabricator-core-user-menu')
->addClass('phabricator-core-user-mobile-menu')
->setNoCSS(true)
->setDropdownMenu($dropdown)
->setAuralLabel(pht('Page Menu'));
}
private function renderApplicationMenu() {
$viewer = $this->getViewer();
$view = $this->getApplicationMenu();
if ($view) {
$items = $view->getItems();
$view = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($items as $item) {
$view->addAction(
id(new PhabricatorActionView())
->setName($item->getName())
->setHref($item->getHref())
->setType($item->getType()));
}
}
return $view;
}
public function renderSearchMenuButton($header_id) {
$button_id = celerity_generate_unique_node_id();
return javelin_tag(
'a',
array(
'class' => 'phabricator-main-menu-search-button '.
'phabricator-expand-application-menu',
'sigil' => 'jx-toggle-class',
'meta' => array(
'map' => array(
$header_id => 'phabricator-search-menu-expanded',
$button_id => 'menu-icon-selected',
),
),
),
phutil_tag(
'span',
array(
'class' => 'phabricator-menu-button-icon phui-icon-view '.
'phui-font-fa fa-search',
'id' => $button_id,
),
''));
}
private function renderPhabricatorSearchMenu() {
$view = new PHUIListView();
$view->addClass('phabricator-search-menu');
$search = $this->renderSearch();
if ($search) {
$view->addMenuItem($search);
}
return $view;
}
private function renderPhabricatorLogo() {
- $custom_header = PhabricatorCustomLogoConfigType::getLogoImagePHID();
-
$logo_style = array();
+ $custom_header = PhabricatorCustomLogoConfigType::getLogoImagePHID();
if ($custom_header) {
- $cache = PhabricatorCaches::getImmutableCache();
- $cache_key_logo = 'ui.custom-header.logo-phid.v3.'.$custom_header;
-
- $logo_uri = $cache->getKey($cache_key_logo);
- if (!$logo_uri) {
- // NOTE: If the file policy has been changed to be restrictive, we'll
- // miss here and just show the default logo. The cache will fill later
- // when someone who can see the file loads the page. This might be a
- // little spooky, see T11982.
- $files = id(new PhabricatorFileQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs(array($custom_header))
- ->execute();
- $file = head($files);
- if ($file) {
- $logo_uri = $file->getViewURI();
- $cache->setKey($cache_key_logo, $logo_uri);
- }
- }
-
- if ($logo_uri) {
- $logo_style[] = 'background-size: 40px 40px;';
- $logo_style[] = 'background-position: 0 0;';
- $logo_style[] = 'background-image: url('.$logo_uri.')';
- }
+ $viewer = $this->getViewer();
+ $logo_uri = PhabricatorCustomLogoConfigType::getLogoURI($viewer);
+ $logo_style[] = 'background-size: 40px 40px;';
+ $logo_style[] = 'background-position: 0 0;';
+ $logo_style[] = 'background-image: url('.$logo_uri.')';
}
$logo_node = phutil_tag(
'span',
array(
'class' => 'phabricator-main-menu-project-logo',
'style' => implode(' ', $logo_style),
));
-
$wordmark_text = PhabricatorCustomLogoConfigType::getLogoWordmark();
if (!phutil_nonempty_string($wordmark_text)) {
$wordmark_text = PlatformSymbols::getPlatformServerName();
}
$wordmark_node = phutil_tag(
'span',
array(
'class' => 'phabricator-wordmark',
),
$wordmark_text);
return phutil_tag(
'a',
array(
'class' => 'phabricator-main-menu-brand',
'href' => '/',
),
array(
javelin_tag(
'span',
array(
'aural' => true,
),
pht('Home')),
$logo_node,
$wordmark_node,
));
}
private function renderNotificationMenu() {
$viewer = $this->getViewer();
require_celerity_resource('phabricator-notification-css');
require_celerity_resource('phabricator-notification-menu-css');
$container_classes = array('alert-notifications');
$aural = array();
$dropdown_query = id(new AphlictDropdownDataQuery())
->setViewer($viewer);
$dropdown_data = $dropdown_query->execute();
$message_tag = '';
$message_notification_dropdown = '';
$conpherence_app = 'PhabricatorConpherenceApplication';
$conpherence_data = $dropdown_data[$conpherence_app];
if ($conpherence_data['isInstalled']) {
$message_id = celerity_generate_unique_node_id();
$message_count_id = celerity_generate_unique_node_id();
$message_dropdown_id = celerity_generate_unique_node_id();
$message_count_number = $conpherence_data['rawCount'];
if ($message_count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/conpherence/',
),
pht(
'%s unread messages.',
new PhutilNumber($message_count_number)));
} else {
$aural[] = pht('No messages.');
}
$message_count_tag = phutil_tag(
'span',
array(
'id' => $message_count_id,
'class' => 'phabricator-main-menu-message-count',
),
$conpherence_data['count']);
$message_icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-message-icon phui-icon-view '.
'phui-font-fa fa-comments',
'sigil' => 'menu-icon',
),
'');
if ($message_count_number) {
$container_classes[] = 'message-unread';
}
$message_tag = phutil_tag(
'a',
array(
'href' => '/conpherence/',
'class' => implode(' ', $container_classes),
'id' => $message_id,
),
array(
$message_icon_tag,
$message_count_tag,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $message_id,
'countID' => $message_count_id,
'dropdownID' => $message_dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/conpherence/panel/',
'countType' => $conpherence_data['countType'],
'countNumber' => $message_count_number,
'unreadClass' => 'message-unread',
) + self::getFavicons());
$message_notification_dropdown = javelin_tag(
'div',
array(
'id' => $message_dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
$bubble_tag = '';
$notification_dropdown = '';
$notification_app = 'PhabricatorNotificationsApplication';
$notification_data = $dropdown_data[$notification_app];
if ($notification_data['isInstalled']) {
$count_id = celerity_generate_unique_node_id();
$dropdown_id = celerity_generate_unique_node_id();
$bubble_id = celerity_generate_unique_node_id();
$count_number = $notification_data['rawCount'];
if ($count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/notification/',
),
pht(
'%s unread notifications.',
new PhutilNumber($count_number)));
} else {
$aural[] = pht('No notifications.');
}
$count_tag = phutil_tag(
'span',
array(
'id' => $count_id,
'class' => 'phabricator-main-menu-alert-count',
),
$notification_data['count']);
$icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-alert-icon phui-icon-view '.
'phui-font-fa fa-bell',
'sigil' => 'menu-icon',
),
'');
if ($count_number) {
$container_classes[] = 'alert-unread';
}
$bubble_tag = phutil_tag(
'a',
array(
'href' => '/notification/',
'class' => implode(' ', $container_classes),
'id' => $bubble_id,
),
array($icon_tag, $count_tag));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $bubble_id,
'countID' => $count_id,
'dropdownID' => $dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/notification/panel/',
'countType' => $notification_data['countType'],
'countNumber' => $count_number,
'unreadClass' => 'alert-unread',
) + self::getFavicons());
$notification_dropdown = javelin_tag(
'div',
array(
'id' => $dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
// Admin Level Urgent Notification Channel
$setup_tag = '';
$setup_notification_dropdown = '';
if ($viewer && $viewer->getIsAdmin()) {
$open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
if ($open) {
$setup_id = celerity_generate_unique_node_id();
$setup_count_id = celerity_generate_unique_node_id();
$setup_dropdown_id = celerity_generate_unique_node_id();
$setup_count_number = count($open);
if ($setup_count_number) {
$aural[] = phutil_tag(
'a',
array(
'href' => '/config/issue/',
),
pht(
'%s unresolved issues.',
new PhutilNumber($setup_count_number)));
} else {
$aural[] = pht('No issues.');
}
$setup_count_tag = phutil_tag(
'span',
array(
'id' => $setup_count_id,
'class' => 'phabricator-main-menu-setup-count',
),
$setup_count_number);
$setup_icon_tag = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
'phui-font-fa fa-exclamation-circle',
'sigil' => 'menu-icon',
),
'');
if ($setup_count_number) {
$container_classes[] = 'setup-unread';
}
$setup_tag = phutil_tag(
'a',
array(
'href' => '/config/issue/',
'class' => implode(' ', $container_classes),
'id' => $setup_id,
),
array(
$setup_icon_tag,
$setup_count_tag,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $setup_id,
'countID' => $setup_count_id,
'dropdownID' => $setup_dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/config/issue/panel/',
'countType' => null,
'countNumber' => null,
'unreadClass' => 'setup-unread',
) + self::getFavicons());
$setup_notification_dropdown = javelin_tag(
'div',
array(
'id' => $setup_dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
}
}
$user_dropdown = null;
$user_tag = null;
if ($viewer->isLoggedIn()) {
if (!$viewer->getIsEmailVerified()) {
$bubble_id = celerity_generate_unique_node_id();
$count_id = celerity_generate_unique_node_id();
$dropdown_id = celerity_generate_unique_node_id();
$settings_uri = id(new PhabricatorEmailAddressesSettingsPanel())
->setViewer($viewer)
->setUser($viewer)
->getPanelURI();
$user_icon = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
'phui-font-fa fa-user',
'sigil' => 'menu-icon',
));
$user_count = javelin_tag(
'span',
array(
'class' => 'phabricator-main-menu-setup-count',
'id' => $count_id,
),
1);
$user_tag = phutil_tag(
'a',
array(
'href' => $settings_uri,
'class' => 'setup-unread',
'id' => $bubble_id,
),
array(
$user_icon,
$user_count,
));
Javelin::initBehavior(
'aphlict-dropdown',
array(
'bubbleID' => $bubble_id,
'countID' => $count_id,
'dropdownID' => $dropdown_id,
'loadingText' => pht('Loading...'),
'uri' => '/settings/issue/',
'unreadClass' => 'setup-unread',
));
$user_dropdown = javelin_tag(
'div',
array(
'id' => $dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
));
}
}
$dropdowns = array(
$notification_dropdown,
$message_notification_dropdown,
$setup_notification_dropdown,
$user_dropdown,
);
return array(
array(
$bubble_tag,
$message_tag,
$setup_tag,
$user_tag,
),
$dropdowns,
$aural,
);
}
private function isFullSession(PhabricatorUser $viewer) {
if (!$viewer->isLoggedIn()) {
return false;
}
if (!$viewer->isUserActivated()) {
return false;
}
if (!$viewer->hasSession()) {
return false;
}
$session = $viewer->getSession();
if ($session->getIsPartial()) {
return false;
}
if (!$session->getSignedLegalpadDocuments()) {
return false;
}
$mfa_key = 'security.require-multi-factor-auth';
$need_mfa = PhabricatorEnv::getEnvConfig($mfa_key);
if ($need_mfa) {
$have_mfa = $viewer->getIsEnrolledInMultiFactor();
if (!$have_mfa) {
return false;
}
}
return true;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 22:01 (6 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129189
Default Alt Text
(78 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment