diff --git a/src/applications/auth/view/PhabricatorAuthAccountView.php b/src/applications/auth/view/PhabricatorAuthAccountView.php index 8eb73144aa..3f04281c8f 100644 --- a/src/applications/auth/view/PhabricatorAuthAccountView.php +++ b/src/applications/auth/view/PhabricatorAuthAccountView.php @@ -1,116 +1,117 @@ externalAccount = $external_account; return $this; } public function setAuthProvider(PhabricatorAuthProvider $provider) { $this->provider = $provider; return $this; } public function render() { $account = $this->externalAccount; $provider = $this->provider; require_celerity_resource('auth-css'); $content = array(); $dispname = $account->getDisplayName(); $username = $account->getUsername(); $realname = $account->getRealName(); $use_name = null; if (strlen($dispname)) { $use_name = $dispname; } else if (strlen($username) && strlen($realname)) { $use_name = $username.' ('.$realname.')'; } else if (strlen($username)) { $use_name = $username; } else if (strlen($realname)) { $use_name = $realname; } else { $use_name = $account->getAccountID(); } $content[] = phutil_tag( 'div', array( 'class' => 'auth-account-view-name', ), $use_name); if ($provider) { $prov_name = pht('%s Account', $provider->getProviderName()); } else { $prov_name = pht('"%s" Account', $account->getProviderType()); } $content[] = phutil_tag( 'div', array( 'class' => 'auth-account-view-provider-name', ), array( $prov_name, " \xC2\xB7 ", $account->getAccountID(), )); $account_uri = $account->getAccountURI(); if (strlen($account_uri)) { // Make sure we don't link a "javascript:" URI if a user somehow // managed to get one here. if (PhabricatorEnv::isValidRemoteURIForLink($account_uri)) { $account_uri = phutil_tag( 'a', array( 'href' => $account_uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $account_uri); } $content[] = phutil_tag( 'div', array( 'class' => 'auth-account-view-account-uri', ), $account_uri); } $image_file = $account->getProfileImageFile(); $xform = PhabricatorFileTransform::getTransformByKey( PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); $image_uri = $image_file->getURIForTransform($xform); list($x, $y) = $xform->getTransformedDimensions($image_file); $profile_image = phutil_tag( 'div', array( 'class' => 'auth-account-view-profile-image', 'style' => 'background-image: url('.$image_uri.');', )); return phutil_tag( 'div', array( 'class' => 'auth-account-view', ), array( $profile_image, $content, )); } } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php index 6174603c84..bd52ec5bc2 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php @@ -1,120 +1,121 @@ getParameter($uri_key); // Since the URI may contain a secret hash, don't show it to users who // can not edit the import. $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $import, PhabricatorPolicyCapability::CAN_EDIT); if (!$can_edit) { $uri_display = phutil_tag('em', array(), pht('Restricted')); } else if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) { $uri_display = $uri; } else { $uri_display = phutil_tag( 'a', array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $uri); } $properties->addProperty(pht('Source URI'), $uri_display); } public function newEditEngineFields( PhabricatorEditEngine $engine, PhabricatorCalendarImport $import) { $fields = array(); if ($engine->getIsCreate()) { $fields[] = id(new PhabricatorTextEditField()) ->setKey('uri') ->setLabel(pht('URI')) ->setDescription(pht('URI to import.')) ->setTransactionType( PhabricatorCalendarImportICSURITransaction::TRANSACTIONTYPE) ->setConduitDescription(pht('URI to import.')) ->setConduitTypeDescription(pht('New URI.')); } return $fields; } public function getDisplayName(PhabricatorCalendarImport $import) { return pht('ICS URI'); } public function importEventsFromSource( PhabricatorUser $viewer, PhabricatorCalendarImport $import, $should_queue) { $uri_key = PhabricatorCalendarImportICSURITransaction::PARAMKEY_URI; $uri = $import->getParameter($uri_key); PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorFilesOutboundRequestAction(), 1); $file = PhabricatorFile::newFromFileDownload( $uri, array( 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 'authorPHID' => $import->getAuthorPHID(), 'canCDN' => true, )); $import->newLogMessage( PhabricatorCalendarImportFetchLogType::LOGTYPE, array( 'file.phid' => $file->getPHID(), )); $data = $file->loadFileData(); if ($should_queue && $this->shouldQueueDataImport($data)) { return $this->queueDataImport($import, $data); } return $this->importICSData($viewer, $import, $data); } public function canDisable( PhabricatorUser $viewer, PhabricatorCalendarImport $import) { return true; } } diff --git a/src/applications/files/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php index 9e91bdc096..36089dc57e 100644 --- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php @@ -1,75 +1,69 @@ isFlatText($matches[0])) { return $matches[0]; } $args = array(); $defaults = array( 'uri' => null, 'alt' => null, - 'href' => null, 'width' => null, 'height' => null, ); $trimmed_match = trim($matches[2]); if ($this->isURI($trimmed_match)) { $args['uri'] = new PhutilURI($trimmed_match); } else { $parser = new PhutilSimpleOptions(); $keys = $parser->parse($trimmed_match); $uri_key = ''; foreach (array('src', 'uri', 'url') as $key) { if (array_key_exists($key, $keys)) { $uri_key = $key; } } if ($uri_key) { $args['uri'] = new PhutilURI($keys[$uri_key]); } $args += $keys; } $args += $defaults; - if ($args['href'] && !PhabricatorEnv::isValidURIForLink($args['href'])) { - $args['href'] = null; - } - if ($args['uri']) { $src_uri = id(new PhutilURI('/file/imageproxy/')) ->setQueryParam('uri', (string)$args['uri']); $img = $this->newTag( 'img', array( 'src' => $src_uri, 'alt' => $args['alt'], - 'href' => $args['href'], 'width' => $args['width'], 'height' => $args['height'], - )); + )); return $this->getEngine()->storeText($img); } else { return $matches[0]; } } private function isURI($uri_string) { // Very simple check to make sure it starts with either http or https. // If it does, we'll try to treat it like a valid URI return preg_match('~^https?\:\/\/.*\z~i', $uri_string); } } diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php index 345621f0f5..93f7564033 100644 --- a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php @@ -1,119 +1,120 @@ 'string', 'name' => 'optional string', 'ui.external' => 'optional bool', ); } public function readArtifactHTTPParameter($key, $value) { // TODO: This is hacky and artifact parameters should be replaced more // broadly, likely with EditFields. See T11887. switch ($key) { case 'ui.external': return (bool)$value; } return $value; } public function getArtifactParameterDescriptions() { return array( 'uri' => pht('The URI to store.'), 'name' => pht('Optional label for this URI.'), 'ui.external' => pht( 'If true, display this URI in the UI as an link to '. 'additional build details in an external build system.'), ); } public function getArtifactDataExample() { return array( 'uri' => 'https://buildserver.mycompany.com/build/123/', 'name' => pht('View External Build Results'), 'ui.external' => true, ); } public function renderArtifactSummary(PhabricatorUser $viewer) { return $this->renderLink(); } public function isExternalLink() { $artifact = $this->getBuildArtifact(); return (bool)$artifact->getProperty('ui.external', false); } public function renderLink() { $artifact = $this->getBuildArtifact(); $uri = $artifact->getProperty('uri'); try { $this->validateURI($uri); } catch (Exception $ex) { return pht(''); } $name = $artifact->getProperty('name', $uri); return phutil_tag( 'a', array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $name); } public function willCreateArtifact(PhabricatorUser $actor) { $artifact = $this->getBuildArtifact(); $uri = $artifact->getProperty('uri'); $this->validateURI($uri); } private function validateURI($raw_uri) { $uri = new PhutilURI($raw_uri); $protocol = $uri->getProtocol(); if (!strlen($protocol)) { throw new Exception( pht( 'Unable to identify the protocol for URI "%s". URIs must be '. 'fully qualified and have an identifiable protocol.', $raw_uri)); } $protocol_key = 'uri.allowed-protocols'; $protocols = PhabricatorEnv::getEnvConfig($protocol_key); if (empty($protocols[$protocol])) { throw new Exception( pht( 'URI "%s" does not have an allowable protocol. Configure '. 'protocols in `%s`. Allowed protocols are: %s.', $raw_uri, $protocol_key, implode(', ', array_keys($protocols)))); } } } diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php index b8bfb3ccab..1f6249f222 100644 --- a/src/applications/nuance/item/NuanceGitHubEventItemType.php +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -1,450 +1,452 @@ newRawEvent($item)->getEventFullTitle(); } public function canUpdateItems() { return true; } protected function updateItemFromSource(NuanceItem $item) { $viewer = $this->getViewer(); $is_dirty = false; $xobj = $this->reloadExternalObject($item); if ($xobj) { $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); $is_dirty = true; } $actor = $this->reloadExternalActor($item); if ($actor) { $item->setRequestorPHID($actor->getPHID()); $is_dirty = true; } if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { $item->setStatus(NuanceItem::STATUS_ROUTING); $is_dirty = true; } if ($is_dirty) { $item->save(); } } private function getDoorkeeperActorRef(NuanceItem $item) { $raw = $this->newRawEvent($item); $user_id = $raw->getActorGitHubUserID(); if (!$user_id) { return null; } $ref_type = DoorkeeperBridgeGitHubUser::OBJTYPE_GITHUB_USER; return $this->newDoorkeeperRef() ->setObjectType($ref_type) ->setObjectID($user_id); } private function getDoorkeeperRef(NuanceItem $item) { $raw = $this->newRawEvent($item); $full_repository = $raw->getRepositoryFullName(); if (!strlen($full_repository)) { return null; } if ($raw->isIssueEvent()) { $ref_type = DoorkeeperBridgeGitHubIssue::OBJTYPE_GITHUB_ISSUE; $issue_number = $raw->getIssueNumber(); $full_ref = "{$full_repository}#{$issue_number}"; } else { return null; } return $this->newDoorkeeperRef() ->setObjectType($ref_type) ->setObjectID($full_ref); } private function newDoorkeeperRef() { return id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB); } private function reloadExternalObject(NuanceItem $item, $local = false) { $ref = $this->getDoorkeeperRef($item); if (!$ref) { return null; } $xobj = $this->reloadExternalRef($item, $ref, $local); if ($xobj) { $this->externalObject = $xobj; } return $xobj; } private function reloadExternalActor(NuanceItem $item, $local = false) { $ref = $this->getDoorkeeperActorRef($item); if (!$ref) { return null; } $xobj = $this->reloadExternalRef($item, $ref, $local); if ($xobj) { $this->externalActor = $xobj; } return $xobj; } private function reloadExternalRef( NuanceItem $item, DoorkeeperObjectRef $ref, $local) { $source = $item->getSource(); $token = $source->getSourceProperty('github.token'); $token = new PhutilOpaqueEnvelope($token); $viewer = $this->getViewer(); $ref = id(new DoorkeeperImportEngine()) ->setViewer($viewer) ->setRefs(array($ref)) ->setThrowOnMissingLink(true) ->setContextProperty('github.token', $token) ->needLocalOnly($local) ->executeOne(); if ($ref->getSyncFailed()) { $xobj = null; } else { $xobj = $ref->getExternalObject(); } return $xobj; } private function getExternalObject(NuanceItem $item) { if ($this->externalObject === null) { $xobj = $this->reloadExternalObject($item, $local = true); if ($xobj) { $this->externalObject = $xobj; } else { $this->externalObject = false; } } if ($this->externalObject) { return $this->externalObject; } return null; } private function getExternalActor(NuanceItem $item) { if ($this->externalActor === null) { $xobj = $this->reloadExternalActor($item, $local = true); if ($xobj) { $this->externalActor = $xobj; } else { $this->externalActor = false; } } if ($this->externalActor) { return $this->externalActor; } return null; } private function newRawEvent(NuanceItem $item) { $type = $item->getItemProperty('api.type'); $raw = $item->getItemProperty('api.raw', array()); return NuanceGitHubRawEvent::newEvent($type, $raw); } public function getItemActions(NuanceItem $item) { $actions = array(); $xobj = $this->getExternalObject($item); if ($xobj) { $actions[] = $this->newItemAction($item, 'reload') ->setName(pht('Reload from GitHub')) ->setIcon('fa-refresh') ->setWorkflow(true) ->setRenderAsForm(true); } $actions[] = $this->newItemAction($item, 'sync') ->setName(pht('Import to Maniphest')) ->setIcon('fa-anchor') ->setWorkflow(true) ->setRenderAsForm(true); $actions[] = $this->newItemAction($item, 'raw') ->setName(pht('View Raw Event')) ->setWorkflow(true) ->setIcon('fa-code'); return $actions; } public function getItemCurtainPanels(NuanceItem $item) { $viewer = $this->getViewer(); $panels = array(); $xobj = $this->getExternalObject($item); if ($xobj) { $xobj_phid = $xobj->getPHID(); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withBridgedObjectPHIDs(array($xobj_phid)) ->executeOne(); if ($task) { $panels[] = $this->newCurtainPanel($item) ->setHeaderText(pht('Imported As')) ->appendChild($viewer->renderHandle($task->getPHID())); } } $xactor = $this->getExternalActor($item); if ($xactor) { $panels[] = $this->newCurtainPanel($item) ->setHeaderText(pht('GitHub Actor')) ->appendChild($viewer->renderHandle($xactor->getPHID())); } return $panels; } protected function handleAction(NuanceItem $item, $action) { $viewer = $this->getViewer(); $controller = $this->getController(); switch ($action) { case 'raw': $raw = array( 'api.type' => $item->getItemProperty('api.type'), 'api.raw' => $item->getItemProperty('api.raw'), ); $raw_output = id(new PhutilJSON())->encodeFormatted($raw); $raw_box = id(new AphrontFormTextAreaControl()) ->setCustomClass('PhabricatorMonospaced') ->setLabel(pht('Raw Event')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($raw_output); $form = id(new AphrontFormView()) ->appendChild($raw_box); return $controller->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('GitHub Raw Event')) ->appendForm($form) ->addCancelButton($item->getURI(), pht('Done')); case 'sync': case 'reload': $item->issueCommand($viewer->getPHID(), $action); return id(new AphrontRedirectResponse())->setURI($item->getURI()); } return null; } protected function newItemView(NuanceItem $item) { $content = array(); $content[] = $this->newGitHubEventItemPropertyBox($item); return $content; } private function newGitHubEventItemPropertyBox($item) { $viewer = $this->getViewer(); $property_list = id(new PHUIPropertyListView()) ->setViewer($viewer); $event = $this->newRawEvent($item); $property_list->addProperty( pht('GitHub Event ID'), $event->getID()); $event_uri = $event->getURI(); if ($event_uri && PhabricatorEnv::isValidRemoteURIForLink($event_uri)) { $event_uri = phutil_tag( 'a', array( 'href' => $event_uri, + 'target' => '_blank', + 'rel' => 'noreferrer', ), $event_uri); } if ($event_uri) { $property_list->addProperty( pht('GitHub Event URI'), $event_uri); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Event Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($property_list); } protected function handleCommand( NuanceItem $item, NuanceItemCommand $command) { // TODO: This code is no longer reachable, and has moved to // CommandImplementation subclasses. $action = $command->getCommand(); switch ($action) { case 'sync': return $this->syncItem($item, $command); case 'reload': $this->reloadExternalObject($item); return true; } return null; } private function syncItem( NuanceItem $item, NuanceItemCommand $command) { $xobj_phid = $item->getItemProperty('doorkeeper.xobj.phid'); if (!$xobj_phid) { throw new Exception( pht( 'Unable to sync: no external object PHID.')); } // TODO: Write some kind of marker to prevent double-synchronization. $viewer = $this->getViewer(); $xobj = id(new DoorkeeperExternalObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($xobj_phid)) ->executeOne(); if (!$xobj) { throw new Exception( pht( 'Unable to sync: failed to load object "%s".', $xobj_phid)); } $acting_as_phid = $this->getActingAsPHID($item); $xactions = array(); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withBridgedObjectPHIDs(array($xobj_phid)) ->executeOne(); if (!$task) { $task = ManiphestTask::initializeNewTask($viewer) ->setAuthorPHID($acting_as_phid) ->setBridgedObjectPHID($xobj_phid); $title = $xobj->getProperty('task.title'); if (!strlen($title)) { $title = pht('Nuance Item %d Task', $item->getID()); } $description = $xobj->getProperty('task.description'); $created = $xobj->getProperty('task.created'); $state = $xobj->getProperty('task.state'); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType( ManiphestTaskTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title) ->setDateCreated($created); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType( ManiphestTaskDescriptionTransaction::TRANSACTIONTYPE) ->setNewValue($description) ->setDateCreated($created); $task->setDateCreated($created); // TODO: Synchronize state. } $event = $this->newRawEvent($item); $comment = $event->getComment(); if (strlen($comment)) { $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent($comment)); } $agent_phid = $command->getAuthorPHID(); $source = $this->newContentSource($item, $agent_phid); $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) ->setActingAsPHID($acting_as_phid) ->setContentSource($source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = $editor->applyTransactions($task, $xactions); return array( 'objectPHID' => $task->getPHID(), 'xactionPHIDs' => mpull($xactions, 'getPHID'), ); } protected function getActingAsPHID(NuanceItem $item) { $actor_phid = $item->getRequestorPHID(); if ($actor_phid) { return $actor_phid; } return parent::getActingAsPHID($item); } } diff --git a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php index 4ef59b300c..c4ffc366da 100644 --- a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php +++ b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php @@ -1,74 +1,75 @@ getEngine(); $viewer = $engine->getConfig('viewer'); $text_mode = $engine->isTextMode(); $html_mode = $engine->isHTMLMailMode(); if (!$this->isFlatText($matches[0])) { return $matches[0]; } $ref = $matches[1]; $monogram = null; $is_monogram = '/^U(?P[1-9]\d*)/'; $query = id(new PhabricatorPhurlURLQuery()) ->setViewer($viewer); if (preg_match($is_monogram, $ref, $monogram)) { $query->withIDs(array($monogram[1])); } else if (ctype_digit($ref)) { $query->withIDs(array($ref)); } else { $query->withAliases(array($ref)); } $phurl = $query->executeOne(); if (!$phurl) { return $matches[0]; } $uri = $phurl->getRedirectURI(); $name = $phurl->getDisplayName(); if ($text_mode || $html_mode) { $uri = PhabricatorEnv::getProductionURI($uri); } if ($text_mode) { return pht( '%s <%s>', $name, $uri); } else { $link = phutil_tag( 'a', array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $name); } return $this->getEngine()->storeText($link); } } diff --git a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php index f790816c8b..0b6a2f330e 100644 --- a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php @@ -1,158 +1,159 @@ getLinkName($config); } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey(self::FIELD_NAME) ->setLabel(pht('Name')) ->setIsRequired(true) ->setValue($this->getLinkName($config)), id(new PhabricatorTextEditField()) ->setKey(self::FIELD_URI) ->setLabel(pht('URI')) ->setIsRequired(true) ->setValue($this->getLinkURI($config)), id(new PhabricatorTextEditField()) ->setKey(self::FIELD_TOOLTIP) ->setLabel(pht('Tooltip')) ->setValue($this->getLinkTooltip($config)), id(new PhabricatorIconSetEditField()) ->setKey('icon') ->setLabel(pht('Icon')) ->setIconSet(new PhabricatorProfileMenuItemIconSet()) ->setValue($this->getLinkIcon($config)), ); } private function getLinkName( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('name'); } private function getLinkIcon( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('icon', 'link'); } private function getLinkURI( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('uri'); } private function getLinkTooltip( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('tooltip'); } private function isValidLinkURI($uri) { return PhabricatorEnv::isValidURIForLink($uri); } protected function newNavigationMenuItems( PhabricatorProfileMenuItemConfiguration $config) { $icon = $this->getLinkIcon($config); $name = $this->getLinkName($config); $href = $this->getLinkURI($config); $tooltip = $this->getLinkTooltip($config); if (!$this->isValidLinkURI($href)) { $href = '#'; } $icon_object = id(new PhabricatorProfileMenuItemIconSet()) ->getIcon($icon); if ($icon_object) { $icon_class = $icon_object->getIcon(); } else { $icon_class = 'fa-link'; } $item = $this->newItem() ->setHref($href) ->setName($name) ->setIcon($icon_class) - ->setTooltip($tooltip); + ->setTooltip($tooltip) + ->setRel('noreferrer'); return array( $item, ); } public function validateTransactions( PhabricatorProfileMenuItemConfiguration $config, $field_key, $value, array $xactions) { $viewer = $this->getViewer(); $errors = array(); if ($field_key == self::FIELD_NAME) { if ($this->isEmptyTransaction($value, $xactions)) { $errors[] = $this->newRequiredError( pht('You must choose a link name.'), $field_key); } } if ($field_key == self::FIELD_URI) { if ($this->isEmptyTransaction($value, $xactions)) { $errors[] = $this->newRequiredError( pht('You must choose a URI to link to.'), $field_key); } foreach ($xactions as $xaction) { $new = $xaction['new']; if (!$new) { continue; } if ($new === $value) { continue; } if (!$this->isValidLinkURI($new)) { $errors[] = $this->newInvalidError( pht( 'URI "%s" is not a valid link URI. It should be a full, valid '. 'URI beginning with a protocol like "%s".', $new, 'https://'), $xaction['xaction']); } } } return $errors; } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index c2b9d6543c..146f34ab07 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -1,105 +1,109 @@ getFieldValue(); if (strlen($value)) { $indexes[] = $this->newStringIndex($value); } return $indexes; } public function renderPropertyViewValue(array $handles) { $value = $this->getFieldValue(); if (!strlen($value)) { return null; } if (!PhabricatorEnv::isValidRemoteURIForLink($value)) { return $value; } return phutil_tag( 'a', - array('href' => $value, 'target' => '_blank'), + array( + 'href' => $value, + 'target' => '_blank', + 'rel' => 'noreferrer', + ), $value); } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return $request->getStr($this->getFieldKey()); } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if (is_string($value) && !strlen($value)) { return; } $value = (array)$value; if ($value) { $query->withApplicationSearchContainsConstraint( $this->newStringIndex(null), $value); } } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, $value) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) ->setValue($value)); } public function shouldAppearInHerald() { return true; } public function getHeraldFieldConditions() { return array( HeraldAdapter::CONDITION_CONTAINS, HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, HeraldAdapter::CONDITION_NOT_REGEXP, ); } public function getHeraldFieldStandardType() { return HeraldField::STANDARD_TEXT; } protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } protected function newConduitSearchParameterType() { return new ConduitStringListParameterType(); } protected function newConduitEditParameterType() { return new ConduitStringParameterType(); } } diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index f6de8eca5b..a1d8fe2664 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -1,371 +1,374 @@ selected = $selected; return $this; } public function getSelected() { return $this->selected; } public function setMetadata($metadata) { $this->metadata = $metadata; return $this; } public function getMetadata() { return $this->metadata; } public function setDownload($download) { $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setHref($href) { $this->href = $href; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function addSigil($sigil) { $this->sigils[] = $sigil; return $this; } public function getHref() { return $this->href; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setLabel($label) { $this->label = $label; return $this; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function setRenderAsForm($form) { $this->renderAsForm = $form; return $this; } public function setOpenInNewWindow($open_in_new_window) { $this->openInNewWindow = $open_in_new_window; return $this; } public function getOpenInNewWindow() { return $this->openInNewWindow; } public function setID($id) { $this->id = $id; return $this; } public function getID() { if (!$this->id) { $this->id = celerity_generate_unique_node_id(); } return $this->id; } public function setOrder($order) { $this->order = $order; return $this; } public function getOrder() { return $this->order; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setSubmenu(array $submenu) { $this->submenu = $submenu; if (!$this->getHref()) { $this->setHref('#'); } return $this; } public function getItems($depth = 0) { $items = array(); $items[] = $this; foreach ($this->submenu as $action) { foreach ($action->getItems($depth + 1) as $item) { $item ->setHidden(true) ->setDepth($depth + 1); $items[] = $item; } } return $items; } public function setHidden($hidden) { $this->hidden = $hidden; return $this; } public function getHidden() { return $this->hidden; } public function setDepth($depth) { $this->depth = $depth; return $this; } public function getDepth() { return $this->depth; } public function render() { $caret_id = celerity_generate_unique_node_id(); $icon = null; if ($this->icon) { $color = ''; if ($this->disabled) { $color = ' grey'; } $icon = id(new PHUIIconView()) ->addClass('phabricator-action-view-icon') ->setIcon($this->icon.$color); } $sigils = array(); if ($this->workflow) { $sigils[] = 'workflow'; } if ($this->download) { $sigils[] = 'download'; } if ($this->submenu) { $sigils[] = 'keep-open'; } if ($this->sigils) { $sigils = array_merge($sigils, $this->sigils); } $sigils = $sigils ? implode(' ', $sigils) : null; if ($this->href) { if ($this->renderAsForm) { if (!$this->hasViewer()) { throw new Exception( pht( 'Call %s when rendering an action as a form.', 'setViewer()')); } $item = javelin_tag( 'button', array( 'class' => 'phabricator-action-view-item', ), array($icon, $this->name)); $item = phabricator_form( $this->getViewer(), array( 'action' => $this->getHref(), 'method' => 'POST', 'sigil' => $sigils, 'meta' => $this->metadata, ), $item); } else { if ($this->getOpenInNewWindow()) { $target = '_blank'; + $rel = 'noreferrer'; } else { $target = null; + $rel = null; } if ($this->submenu) { $caret = javelin_tag( 'span', array( 'class' => 'caret-right', 'id' => $caret_id, ), ''); } else { $caret = null; } $item = javelin_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'phabricator-action-view-item', 'target' => $target, + 'rel' => $rel, 'sigil' => $sigils, 'meta' => $this->metadata, ), array($icon, $this->name, $caret)); } } else { $item = javelin_tag( 'span', array( 'class' => 'phabricator-action-view-item', 'sigil' => $sigils, ), array($icon, $this->name, $this->renderChildren())); } $classes = array(); $classes[] = 'phabricator-action-view'; if ($this->disabled) { $classes[] = 'phabricator-action-view-disabled'; } if ($this->label) { $classes[] = 'phabricator-action-view-label'; } if ($this->selected) { $classes[] = 'phabricator-action-view-selected'; } if ($this->submenu) { $classes[] = 'phabricator-action-view-submenu'; } if ($this->getHref()) { $classes[] = 'phabricator-action-view-href'; } if ($this->icon) { $classes[] = 'action-has-icon'; } if ($this->color) { $classes[] = $this->color; } if ($this->type) { $classes[] = 'phabricator-action-view-'.$this->type; } $style = array(); if ($this->hidden) { $style[] = 'display: none;'; } if ($this->depth) { $indent = ($this->depth * 16); $style[] = "margin-left: {$indent}px;"; } $sigil = null; $meta = null; if ($this->submenu) { Javelin::initBehavior('phui-submenu'); $sigil = 'phui-submenu'; $item_ids = array(); foreach ($this->submenu as $subitem) { $item_ids[] = $subitem->getID(); } $meta = array( 'itemIDs' => $item_ids, 'caretID' => $caret_id, ); } return javelin_tag( 'li', array( 'id' => $this->getID(), 'class' => implode(' ', $classes), 'style' => implode(' ', $style), 'sigil' => $sigil, 'meta' => $meta, ), $item); } } diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php index e8a1940737..8e53024826 100644 --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -1,379 +1,390 @@ openInNewWindow = $open_in_new_window; return $this; } public function getOpenInNewWindow() { return $this->openInNewWindow; } - public function setHideInApplicationMenu($hide) { + public function setRel($rel) { + $this->rel = $rel; + return $this; + } + + public function getRel() { + return $this->rel; + } + + public function setHideInApplicationMenu($hide) { $this->hideInApplicationMenu = $hide; return $this; } public function getHideInApplicationMenu() { return $this->hideInApplicationMenu; } public function setDropdownMenu(PhabricatorActionListView $actions) { Javelin::initBehavior('phui-dropdown-menu'); $this->addSigil('phui-dropdown-menu'); $this->setMetadata($actions->getDropdownMenuMetadata()); return $this; } public function setAural($aural) { $this->aural = $aural; return $this; } public function getAural() { return $this->aural; } public function setOrder($order) { $this->order = $order; return $this; } public function getOrder() { return $this->order; } public function setRenderNameAsTooltip($render_name_as_tooltip) { $this->renderNameAsTooltip = $render_name_as_tooltip; return $this; } public function getRenderNameAsTooltip() { return $this->renderNameAsTooltip; } public function setSelected($selected) { $this->selected = $selected; return $this; } public function getSelected() { return $this->selected; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setProfileImage($image) { $this->profileImage = $image; return $this; } public function getIcon() { return $this->icon; } public function setCount($count) { $this->count = $count; return $this; } public function setIndented($indented) { $this->indented = $indented; return $this; } public function getIndented() { return $this->indented; } public function setKey($key) { $this->key = (string)$key; return $this; } public function getKey() { return $this->key; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setActionIcon($icon, $href) { $this->actionIcon = $icon; $this->actionIconHref = $href; return $this; } public function setIsExternal($is_external) { $this->isExternal = $is_external; return $this; } public function getIsExternal() { return $this->isExternal; } public function setStatusColor($color) { $this->statusColor = $color; return $this; } public function addIcon($icon) { $this->icons[] = $icon; return $this; } public function getIcons() { return $this->icons; } public function setTooltip($tooltip) { $this->tooltip = $tooltip; return $this; } protected function getTagName() { return 'li'; } protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-list-item-view'; $classes[] = 'phui-list-item-'.$this->type; if ($this->icon || $this->profileImage) { $classes[] = 'phui-list-item-has-icon'; } if ($this->selected) { $classes[] = 'phui-list-item-selected'; } if ($this->disabled) { $classes[] = 'phui-list-item-disabled'; } if ($this->statusColor) { $classes[] = $this->statusColor; } if ($this->actionIcon) { $classes[] = 'phui-list-item-has-action-icon'; } return array( 'class' => implode(' ', $classes), ); } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } protected function getTagContent() { $name = null; $icon = null; $meta = null; $sigil = null; if ($this->name) { if ($this->getRenderNameAsTooltip()) { Javelin::initBehavior('phabricator-tooltips'); $sigil = 'has-tooltip'; $meta = array( 'tip' => $this->name, 'align' => 'E', ); } else { if ($this->tooltip) { Javelin::initBehavior('phabricator-tooltips'); $sigil = 'has-tooltip'; $meta = array( 'tip' => $this->tooltip, 'align' => 'E', 'size' => 300, ); } $external = null; if ($this->isExternal) { $external = " \xE2\x86\x97"; } // If this element has an aural representation, make any name visual // only. This is primarily dealing with the links in the main menu like // "Profile" and "Logout". If we don't hide the name, the mobile // version of these elements will have two redundant names. $classes = array(); $classes[] = 'phui-list-item-name'; if ($this->aural !== null) { $classes[] = 'visual-only'; } $name = phutil_tag( 'span', array( 'class' => implode(' ', $classes), ), array( $this->name, $external, )); } } $aural = null; if ($this->aural !== null) { $aural = javelin_tag( 'span', array( 'aural' => true, ), $this->aural); } if ($this->icon) { $icon_name = $this->icon; if ($this->getDisabled()) { $icon_name .= ' grey'; } $icon = id(new PHUIIconView()) ->addClass('phui-list-item-icon') ->setIcon($icon_name); } if ($this->profileImage) { $icon = id(new PHUIIconView()) ->setHeadSize(PHUIIconView::HEAD_SMALL) ->addClass('phui-list-item-icon') ->setImage($this->profileImage); } $classes = array(); if ($this->href) { $classes[] = 'phui-list-item-href'; } if ($this->indented) { $classes[] = 'phui-list-item-indented'; } $action_link = null; if ($this->actionIcon) { $action_icon = id(new PHUIIconView()) ->setIcon($this->actionIcon) ->addClass('phui-list-item-action-icon'); $action_link = phutil_tag( 'a', array( 'href' => $this->actionIconHref, 'class' => 'phui-list-item-action-href', ), $action_icon); } $count = null; if ($this->count) { $count = phutil_tag( 'span', array( 'class' => 'phui-list-item-count', ), $this->count); } $icons = $this->getIcons(); $list_item = javelin_tag( $this->href ? 'a' : 'div', array( 'href' => $this->href, 'class' => implode(' ', $classes), 'meta' => $meta, 'sigil' => $sigil, 'target' => $this->getOpenInNewWindow() ? '_blank' : null, + 'rel' => $this->rel, ), array( $aural, $icon, $icons, $this->renderChildren(), $name, $count, )); return array($list_item, $action_link); } } diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php index 292246c4a7..482b3f4400 100644 --- a/src/view/phui/PHUITagView.php +++ b/src/view/phui/PHUITagView.php @@ -1,312 +1,317 @@ type = $type; switch ($type) { case self::TYPE_SHADE: case self::TYPE_OUTLINE: break; case self::TYPE_OBJECT: $this->setBackgroundColor(self::COLOR_OBJECT); break; case self::TYPE_PERSON: $this->setBackgroundColor(self::COLOR_PERSON); break; } return $this; } /** * This method has been deprecated, use @{method:setColor} instead. * * @deprecated */ public function setShade($shade) { phlog( pht('Deprecated call to setShade(), use setColor() instead.')); $this->color = $shade; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function setDotColor($dot_color) { $this->dotColor = $dot_color; return $this; } public function setBackgroundColor($background_color) { $this->backgroundColor = $background_color; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } public function setBorder($border) { $this->border = $border; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setSlimShady($mm) { $this->slimShady = $mm; return $this; } protected function getTagName() { return strlen($this->href) ? 'a' : 'span'; } protected function getTagAttributes() { require_celerity_resource('phui-tag-view-css'); $classes = array( 'phui-tag-view', 'phui-tag-type-'.$this->type, ); if ($this->color) { $classes[] = 'phui-tag-'.$this->color; } if ($this->slimShady) { $classes[] = 'phui-tag-slim'; } if ($this->type == self::TYPE_SHADE) { $classes[] = 'phui-tag-shade'; } if ($this->icon) { $classes[] = 'phui-tag-icon-view'; } if ($this->border) { $classes[] = 'phui-tag-'.$this->border; } + $attributes = array( + 'href' => $this->href, + 'class' => $classes, + ); + + if ($this->external) { + $attributes += array( + 'target' => '_blank', + 'rel' => 'noreferrer', + ); + } + if ($this->phid) { Javelin::initBehavior('phui-hovercards'); - $attributes = array( - 'href' => $this->href, + $attributes += array( 'sigil' => 'hovercard', - 'meta' => array( + 'meta' => array( 'hoverPHID' => $this->phid, ), - 'target' => $this->external ? '_blank' : null, - ); - } else { - $attributes = array( - 'href' => $this->href, - 'target' => $this->external ? '_blank' : null, ); } - return $attributes + array('class' => $classes); + return $attributes; } protected function getTagContent() { if (!$this->type) { throw new PhutilInvalidStateException('setType', 'render'); } $color = null; if (!$this->shade && $this->backgroundColor) { $color = 'phui-tag-color-'.$this->backgroundColor; } if ($this->dotColor) { $dotcolor = 'phui-tag-color-'.$this->dotColor; $dot = phutil_tag( 'span', array( 'class' => 'phui-tag-dot '.$dotcolor, ), ''); } else { $dot = null; } if ($this->icon) { $icon = id(new PHUIIconView()) ->setIcon($this->icon); } else { $icon = null; } $content = phutil_tag( 'span', array( 'class' => 'phui-tag-core '.$color, ), array($dot, $icon, $this->name)); if ($this->closed) { $content = phutil_tag( 'span', array( 'class' => 'phui-tag-core-closed', ), array($icon, $content)); } return $content; } public static function getTagTypes() { return array( self::TYPE_PERSON, self::TYPE_OBJECT, self::TYPE_STATE, ); } public static function getColors() { return array( self::COLOR_RED, self::COLOR_ORANGE, self::COLOR_YELLOW, self::COLOR_BLUE, self::COLOR_INDIGO, self::COLOR_VIOLET, self::COLOR_GREEN, self::COLOR_BLACK, self::COLOR_GREY, self::COLOR_WHITE, self::COLOR_OBJECT, self::COLOR_PERSON, ); } public static function getShades() { return array_keys(self::getShadeMap()); } public static function getShadeMap() { return array( self::COLOR_RED => pht('Red'), self::COLOR_ORANGE => pht('Orange'), self::COLOR_YELLOW => pht('Yellow'), self::COLOR_BLUE => pht('Blue'), self::COLOR_INDIGO => pht('Indigo'), self::COLOR_VIOLET => pht('Violet'), self::COLOR_GREEN => pht('Green'), self::COLOR_GREY => pht('Grey'), self::COLOR_PINK => pht('Pink'), self::COLOR_CHECKERED => pht('Checkered'), self::COLOR_DISABLED => pht('Disabled'), ); } public static function getShadeName($shade) { return idx(self::getShadeMap(), $shade, $shade); } public static function getOutlines() { return array_keys(self::getOutlineMap()); } public static function getOutlineMap() { return array( self::COLOR_RED => pht('Red'), self::COLOR_ORANGE => pht('Orange'), self::COLOR_YELLOW => pht('Yellow'), self::COLOR_BLUE => pht('Blue'), self::COLOR_INDIGO => pht('Indigo'), self::COLOR_VIOLET => pht('Violet'), self::COLOR_GREEN => pht('Green'), self::COLOR_GREY => pht('Grey'), self::COLOR_PINK => pht('Pink'), self::COLOR_SKY => pht('Sky'), self::COLOR_FIRE => pht('Fire'), self::COLOR_BLACK => pht('Black'), self::COLOR_DISABLED => pht('Disabled'), ); } public static function getOutlineName($outline) { return idx(self::getOutlineMap(), $outline, $outline); } public function setExternal($external) { $this->external = $external; return $this; } public function getExternal() { return $this->external; } }