Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/aphront/site/PhabricatorPlatformSite.php b/src/aphront/site/PhabricatorPlatformSite.php
index 21e2cd2529..63c2b9cde3 100644
--- a/src/aphront/site/PhabricatorPlatformSite.php
+++ b/src/aphront/site/PhabricatorPlatformSite.php
@@ -1,33 +1,40 @@
<?php
final class PhabricatorPlatformSite extends PhabricatorSite {
public function getDescription() {
return pht('Serves the core platform and applications.');
}
public function getPriority() {
return 1000;
}
public function newSiteForRequest(AphrontRequest $request) {
+ // If no base URI has been configured yet, match this site so the user
+ // can follow setup instructions.
+ $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
+ if (!strlen($base_uri)) {
+ return new PhabricatorPlatformSite();
+ }
+
$uris = array();
- $uris[] = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
+ $uris[] = $base_uri;
$uris[] = PhabricatorEnv::getEnvConfig('phabricator.production-uri');
$allowed = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
if ($allowed) {
foreach ($allowed as $uri) {
$uris[] = $uri;
}
}
$host = $request->getHost();
if ($this->isHostMatch($host, $uris)) {
return new PhabricatorPlatformSite();
}
return null;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index 03b55df5db..d91690a570 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,535 +1,541 @@
<?php
/**
* Manages rendering and aggregation of a story. A story is an event (like a
* user adding a comment) which may be represented in different forms on
* different channels (like feed, notifications and realtime alerts).
*
* @task load Loading Stories
* @task policy Policy Implementation
*/
abstract class PhabricatorFeedStory
extends Phobject
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface {
private $data;
private $hasViewed;
private $framed;
private $hovercard = false;
private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML;
private $handles = array();
private $objects = array();
private $projectPHIDs = array();
private $markupFieldOutput = array();
/* -( Loading Stories )---------------------------------------------------- */
/**
* Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
* construct appropriate @{class:PhabricatorFeedStory} wrappers for each
* data row.
*
* @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the
* database.
* @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}
* objects.
* @task load
*/
public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
$stories = array();
$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
foreach ($data as $story_data) {
$class = $story_data->getStoryType();
try {
$ok =
class_exists($class) &&
is_subclass_of($class, __CLASS__);
} catch (PhutilMissingSymbolException $ex) {
$ok = false;
}
// If the story type isn't a valid class or isn't a subclass of
// PhabricatorFeedStory, decline to load it.
if (!$ok) {
continue;
}
$key = $story_data->getChronologicalKey();
$stories[$key] = newv($class, array($story_data));
}
$object_phids = array();
$key_phids = array();
foreach ($stories as $key => $story) {
$phids = array();
foreach ($story->getRequiredObjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($story->getPrimaryObjectPHID()) {
$phids[$story->getPrimaryObjectPHID()] = true;
}
$key_phids[$key] = $phids;
$object_phids += $phids;
}
- $objects = id(new PhabricatorObjectQuery())
+ $object_query = id(new PhabricatorObjectQuery())
->setViewer($viewer)
- ->withPHIDs(array_keys($object_phids))
- ->execute();
+ ->withPHIDs(array_keys($object_phids));
+
+ $objects = $object_query->execute();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_objects = array_select_keys($objects, array_keys($phids));
if (count($story_objects) != count($phids)) {
// An object this story requires either does not exist or is not visible
// to the user. Decline to render the story.
unset($stories[$key]);
unset($key_phids[$key]);
continue;
}
$stories[$key]->setObjects($story_objects);
}
// If stories are about PhabricatorProjectInterface objects, load the
// projects the objects are a part of so we can render project tags
// on the stories.
$project_phids = array();
foreach ($objects as $object) {
if ($object instanceof PhabricatorProjectInterface) {
$project_phids[$object->getPHID()] = array();
}
}
if ($project_phids) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array_keys($project_phids))
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($project_phids as $phid => $ignored) {
$project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid));
}
}
$handle_phids = array();
foreach ($stories as $key => $story) {
foreach ($story->getRequiredHandlePHIDs() as $phid) {
$key_phids[$key][$phid] = true;
}
if ($story->getAuthorPHID()) {
$key_phids[$key][$story->getAuthorPHID()] = true;
}
$object_phid = $story->getPrimaryObjectPHID();
$object_project_phids = idx($project_phids, $object_phid, array());
$story->setProjectPHIDs($object_project_phids);
foreach ($object_project_phids as $dst) {
$key_phids[$key][$dst] = true;
}
$handle_phids += $key_phids[$key];
}
+ // NOTE: This setParentQuery() is a little sketchy. Ideally, this whole
+ // method should be inside FeedQuery and it should be the parent query of
+ // both subqueries. We're just trying to share the workspace cache.
+
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
+ ->setParentQuery($object_query)
->withPHIDs(array_keys($handle_phids))
->execute();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_handles = array_select_keys($handles, array_keys($phids));
$stories[$key]->setHandles($story_handles);
}
// Load and process story markup blocks.
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($viewer);
foreach ($stories as $story) {
foreach ($story->getFieldStoryMarkupFields() as $field) {
$engine->addObject($story, $field);
}
}
$engine->process();
foreach ($stories as $story) {
foreach ($story->getFieldStoryMarkupFields() as $field) {
$story->setMarkupFieldOutput(
$field,
$engine->getOutput($story, $field));
}
}
return $stories;
}
public function setMarkupFieldOutput($field, $output) {
$this->markupFieldOutput[$field] = $output;
return $this;
}
public function getMarkupFieldOutput($field) {
if (!array_key_exists($field, $this->markupFieldOutput)) {
throw new Exception(
pht(
'Trying to retrieve markup field key "%s", but this feed story '.
'did not request it be rendered.',
$field));
}
return $this->markupFieldOutput[$field];
}
public function setHovercard($hover) {
$this->hovercard = $hover;
return $this;
}
public function setRenderingTarget($target) {
$this->validateRenderingTarget($target);
$this->renderingTarget = $target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
private function validateRenderingTarget($target) {
switch ($target) {
case PhabricatorApplicationTransaction::TARGET_HTML:
case PhabricatorApplicationTransaction::TARGET_TEXT:
break;
default:
throw new Exception(pht('Unknown rendering target: %s', $target));
break;
}
}
public function setObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObject($phid) {
$object = idx($this->objects, $phid);
if (!$object) {
throw new Exception(
pht(
"Story is asking for an object it did not request ('%s')!",
$phid));
}
return $object;
}
public function getPrimaryObject() {
$phid = $this->getPrimaryObjectPHID();
if (!$phid) {
throw new Exception(pht('Story has no primary object!'));
}
return $this->getObject($phid);
}
public function getPrimaryObjectPHID() {
return null;
}
final public function __construct(PhabricatorFeedStoryData $data) {
$this->data = $data;
}
abstract public function renderView();
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher) {
// TODO: This (and text rendering) should be properly abstract and
// universal. However, this is far less bad than it used to be, and we
// need to clean up more old feed code to really make this reasonable.
return pht(
'(Unable to render story of class %s for Doorkeeper.)',
get_class($this));
}
public function getRequiredHandlePHIDs() {
return array();
}
public function getRequiredObjectPHIDs() {
return array();
}
public function setHasViewed($has_viewed) {
$this->hasViewed = $has_viewed;
return $this;
}
public function getHasViewed() {
return $this->hasViewed;
}
final public function setFramed($framed) {
$this->framed = $framed;
return $this;
}
final public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
final protected function getObjects() {
return $this->objects;
}
final protected function getHandles() {
return $this->handles;
}
final protected function getHandle($phid) {
if (isset($this->handles[$phid])) {
if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
return $this->handles[$phid];
}
}
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setName(pht("Unloaded Object '%s'", $phid));
return $handle;
}
final public function getStoryData() {
return $this->data;
}
final public function getEpoch() {
return $this->getStoryData()->getEpoch();
}
final public function getChronologicalKey() {
return $this->getStoryData()->getChronologicalKey();
}
final public function getValue($key, $default = null) {
return $this->getStoryData()->getValue($key, $default);
}
final public function getAuthorPHID() {
return $this->getStoryData()->getAuthorPHID();
}
final protected function renderHandleList(array $phids) {
$items = array();
foreach ($phids as $phid) {
$items[] = $this->linkTo($phid);
}
$list = null;
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
$list = implode(', ', $items);
break;
case PhabricatorApplicationTransaction::TARGET_HTML:
$list = phutil_implode_html(', ', $items);
break;
}
return $list;
}
final protected function linkTo($phid) {
$handle = $this->getHandle($phid);
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
return $handle->getLinkName();
}
// NOTE: We render our own link here to customize the styling and add
// the '_top' target for framed feeds.
$class = null;
if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) {
$class = 'phui-link-person';
}
return javelin_tag(
'a',
array(
'href' => $handle->getURI(),
'target' => $this->framed ? '_top' : null,
'sigil' => $this->hovercard ? 'hovercard' : null,
'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null,
'class' => $class,
),
$handle->getLinkName());
}
final protected function renderString($str) {
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_TEXT:
return $str;
case PhabricatorApplicationTransaction::TARGET_HTML:
return phutil_tag('strong', array(), $str);
}
}
final public function renderSummary($text, $len = 128) {
if ($len) {
$text = id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs($len)
->truncateString($text);
}
switch ($this->getRenderingTarget()) {
case PhabricatorApplicationTransaction::TARGET_HTML:
$text = phutil_escape_html_newlines($text);
break;
}
return $text;
}
public function getNotificationAggregations() {
return array();
}
protected function newStoryView() {
$view = id(new PHUIFeedStoryView())
->setChronologicalKey($this->getChronologicalKey())
->setEpoch($this->getEpoch())
->setViewed($this->getHasViewed());
$project_phids = $this->getProjectPHIDs();
if ($project_phids) {
$view->setTags($this->renderHandleList($project_phids));
}
return $view;
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function getProjectPHIDs() {
return $this->projectPHIDs;
}
public function getFieldStoryMarkupFields() {
return array();
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getPHID() {
return null;
}
/**
* @task policy
*/
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
/**
* @task policy
*/
public function getPolicy($capability) {
$policy_object = $this->getPrimaryPolicyObject();
if ($policy_object) {
return $policy_object->getPolicy($capability);
}
// TODO: Remove this once all objects are policy-aware. For now, keep
// respecting the `feed.public` setting.
return PhabricatorEnv::getEnvConfig('feed.public')
? PhabricatorPolicies::POLICY_PUBLIC
: PhabricatorPolicies::POLICY_USER;
}
/**
* @task policy
*/
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$policy_object = $this->getPrimaryPolicyObject();
if ($policy_object) {
return $policy_object->hasAutomaticCapability($capability, $viewer);
}
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
/**
* Get the policy object this story is about, if such a policy object
* exists.
*
* @return PhabricatorPolicyInterface|null Policy object, if available.
* @task policy
*/
private function getPrimaryPolicyObject() {
$primary_phid = $this->getPrimaryObjectPHID();
if (empty($this->objects[$primary_phid])) {
$object = $this->objects[$primary_phid];
if ($object instanceof PhabricatorPolicyInterface) {
return $object;
}
}
return null;
}
/* -( PhabricatorMarkupInterface Implementation )--------------------------- */
public function getMarkupFieldKey($field) {
return 'feed:'.$this->getChronologicalKey().':'.$field;
}
public function newMarkupEngine($field) {
- return PhabricatorMarkupEngine::newMarkupEngine(array());
+ return PhabricatorMarkupEngine::getEngine();
}
public function getMarkupText($field) {
throw new PhutilMethodNotImplementedException();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return true;
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index b4985bfaa5..4f2b9b5820 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1285 +1,1308 @@
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface,
PhabricatorApplicationTransactionInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $sex;
protected $translation;
protected $passwordSalt;
protected $passwordHash;
protected $profileImagePHID;
protected $profileImageCache;
protected $availabilityCache;
protected $availabilityCacheTTL;
protected $timezoneIdentifier = '';
protected $consoleEnabled = 0;
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isMailingList = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profileImage = self::ATTACHABLE;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
private $authorities = array();
private $handlePool;
private $csrfSalt;
protected function readField($field) {
switch ($field) {
case 'timezoneIdentifier':
// If the user hasn't set one, guess the server's time.
return nonempty(
$this->timezoneIdentifier,
date_default_timezone_get());
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isMailingList':
return (bool)$this->isMailingList;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
public function canEstablishWebSessions() {
if ($this->getIsMailingList()) {
return false;
}
if ($this->getIsSystemAgent()) {
return false;
}
return true;
}
public function canEstablishAPISessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
public function canEstablishSSHSessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'sex' => 'text4?',
'translation' => 'text64?',
'passwordSalt' => 'text32?',
'passwordHash' => 'text128?',
'profileImagePHID' => 'phid?',
'consoleEnabled' => 'bool',
'consoleVisible' => 'bool',
'consoleTab' => 'text64',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isMailingList' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'timezoneIdentifier' => 'text255',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'profileImageCache' => 'text255?',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'profileImageCache' => true,
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
if (!$this->getPHID()) {
throw new Exception(
pht(
'You can not set a password for an unsaved user because their PHID '.
'is a salt component in the password hash.'));
}
if (!strlen($envelope->openEnvelope())) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(Filesystem::readRandomBytes(32)));
$hash = $this->hashPassword($envelope);
$this->setPasswordHash($hash->openEnvelope());
}
return $this;
}
// To satisfy PhutilPerson.
public function getSex() {
return $this->sex;
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = parent::save();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
id(new PhabricatorSearchIndexer())
->queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
if (!strlen($envelope->openEnvelope())) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
return PhabricatorPasswordHasher::comparePassword(
$this->getPasswordHashInput($envelope),
new PhutilOpaqueEnvelope($this->getPasswordHash()));
}
private function getPasswordHashInput(PhutilOpaqueEnvelope $password) {
$input =
$this->getUsername().
$password->openEnvelope().
$this->getPHID().
$this->getPasswordSalt();
return new PhutilOpaqueEnvelope($input);
}
private function hashPassword(PhutilOpaqueEnvelope $password) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
$input_envelope = $this->getPasswordHashInput($password);
return $hasher->getPasswordHashForStorage($input_envelope);
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_SALT_LENGTH = 8;
const CSRF_TOKEN_LENGTH = 16;
const CSRF_BREACH_PREFIX = 'B@';
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
private function getRawCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
}
public function getCSRFToken() {
if ($this->isOmnipotent()) {
// We may end up here when called from the daemons. The omnipotent user
// has no meaningful CSRF token, so just return `null`.
return null;
}
if ($this->csrfSalt === null) {
$this->csrfSalt = Filesystem::readRandomCharacters(
self::CSRF_SALT_LENGTH);
}
$salt = $this->csrfSalt;
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::digest($token, $salt);
return self::CSRF_BREACH_PREFIX.$salt.substr(
$hash, 0, self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
$salt = null;
$version = 'plain';
// This is a BREACH-mitigating token. See T3684.
$breach_prefix = self::CSRF_BREACH_PREFIX;
$breach_prelen = strlen($breach_prefix);
if (!strncmp($token, $breach_prefix, $breach_prelen)) {
$version = 'breach';
$salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
$token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
}
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
switch ($version) {
// TODO: We can remove this after the BREACH version has been in the
// wild for a while.
case 'plain':
if ($token == $valid) {
return true;
}
break;
case 'breach':
$digest = PhabricatorHash::digest($valid, $salt);
if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) {
return true;
}
break;
default:
throw new Exception(pht('Unknown CSRF token format!'));
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
if ($this->getPHID()) {
$vec = $this->getPHID().$this->getAccountSecret();
} else {
$vec = $this->getAlternateCSRFString();
}
if ($this->hasSession()) {
$vec = $vec.$this->getSession()->getSessionKey();
}
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
}
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$profile_dao->setUserPHID($this->getPHID());
$this->profile = $profile_dao;
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception(pht('User has no primary email address!'));
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
public function loadPreferences() {
if ($this->preferences) {
return $this->preferences;
}
$preferences = null;
if ($this->getPHID()) {
$preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
'userPHID = %s',
$this->getPHID());
}
if (!$preferences) {
$preferences = new PhabricatorUserPreferences();
$preferences->setUserPHID($this->getPHID());
$default_dict = array(
PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0,
);
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
public function loadEditorLink($path, $line, $callsign) {
$editor = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_EDITOR);
if (is_array($path)) {
$multiedit = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
switch ($multiedit) {
case '':
$path = implode(' ', $path);
break;
case 'disable':
return null;
}
}
if (!strlen($editor)) {
return null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
public function getAlternateCSRFString() {
return $this->assertAttached($this->alternateCSRFString);
}
public function attachAlternateCSRFString($string) {
$this->alternateCSRFString = $string;
return $this;
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
$this->loadPrimaryEmail(),
PhabricatorAuthSessionEngine::ONETIME_WELCOME);
$body = pht(
"Welcome to Phabricator!\n\n".
"%s (%s) has created an account for you.\n\n".
" Username: %s\n\n".
"To login to Phabricator, follow this link and set a password:\n\n".
" %s\n\n".
"After you have set a password, you can login in the future by ".
"going here:\n\n".
" %s\n",
$admin_username,
$admin_realname,
$user_username,
$uri,
$base_uri);
if (!$is_serious) {
$body .= sprintf(
"\n%s\n",
pht("Love,\nPhabricator"));
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Welcome to Phabricator'))
->setBody($body)
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = sprintf(
"%s\n\n %s\n\n%s\n",
pht(
"If you use a password to login, you'll need to reset it ".
"before you can login again. You can reset your password by ".
"following this link:"),
$uri,
pht(
"And, of course, you'll need to use your new username to login ".
"from now on. If you use OAuth to login, nothing should change."));
}
$body = sprintf(
"%s\n\n %s\n %s\n\n%s",
pht(
'%s (%s) has changed your Phabricator username.',
$admin_username,
$admin_realname),
pht(
'Old Username: %s',
$old_username),
pht(
'New Username: %s',
$new_username),
$password_instructions);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Username Changed'))
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function attachProfileImageURI($uri) {
$this->profileImage = $uri;
return $this;
}
public function getProfileImageURI() {
return $this->assertAttached($this->profileImage);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function getPreference($key) {
$preferences = $this->loadPreferences();
// TODO: After T4103 and T7707 this should eventually be pushed down the
// stack into modular preference definitions and role profiles. This is
// just fixing T8601 and mildly anticipating those changes.
$value = $preferences->getPreference($key);
$allowed_values = null;
switch ($key) {
case PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT:
$allowed_values = array(
'g:i A',
'H:i',
);
break;
case PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT:
$allowed_values = array(
'Y-m-d',
'n/j/Y',
'd-m-Y',
);
break;
}
if ($allowed_values !== null) {
$allowed_values = array_fuse($allowed_values);
if (empty($allowed_values[$value])) {
$value = head($allowed_values);
}
}
return $value;
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
public function getDefaultSpacePHID() {
// TODO: We might let the user switch which space they're "in" later on;
// for now just use the global space if one exists.
// If the viewer has access to the default space, use that.
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
return $space->getPHID();
}
}
// Otherwise, use the space with the lowest ID that they have access to.
// This just tends to keep the default stable and predictable over time,
// so adding a new space won't change behavior for users.
if ($spaces) {
$spaces = msort($spaces, 'getID');
return head($spaces)->getPHID();
}
return null;
}
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
/**
* Describe the user's availability.
*
* @param PhabricatorUser Viewing user.
* @return string Human-readable description of away status.
* @task availability
*/
public function getAvailabilityDescription(PhabricatorUser $viewer) {
$until = $this->getAwayUntil();
if ($until) {
return pht('Away until %s', phabricator_datetime($until, $viewer));
} else {
return pht('Available');
}
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Profile Image Cache )------------------------------------------------ */
/**
* Get this user's cached profile image URI.
*
* @return string|null Cached URI, if a URI is cached.
* @task image-cache
*/
public function getProfileImageCache() {
$version = $this->getProfileImageVersion();
$parts = explode(',', $this->profileImageCache, 2);
if (count($parts) !== 2) {
return null;
}
if ($parts[0] !== $version) {
return null;
}
return $parts[1];
}
/**
* Generate a new cache value for this user's profile image.
*
* @return string New cache value.
* @task image-cache
*/
public function writeProfileImageCache($uri) {
$version = $this->getProfileImageVersion();
$cache = "{$version},{$uri}";
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET profileImageCache = %s WHERE id = %d',
$this->getTableName(),
$cache,
$this->getID());
unset($unguarded);
}
/**
* Get a version identifier for a user's profile image.
*
* This version will change if the image changes, or if any of the
* environment configuration which goes into generating a URI changes.
*
* @return string Cache version.
* @task image-cache
*/
private function getProfileImageVersion() {
$parts = array(
PhabricatorEnv::getCDNURI('/'),
PhabricatorEnv::getEnvConfig('cluster.instance'),
$this->getProfileImagePHID(),
);
$parts = serialize($parts);
return PhabricatorHash::digestForIndex($parts);
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
+ /**
+ * Get a scalar string identifying this user.
+ *
+ * This is similar to using the PHID, but distinguishes between ominpotent
+ * and public users explicitly. This allows safe construction of cache keys
+ * or cache buckets which do not conflate public and omnipotent users.
+ *
+ * @return string Scalar identifier.
+ */
+ public function getCacheFragment() {
+ if ($this->isOmnipotent()) {
+ return 'u.omnipotent';
+ }
+
+ $phid = $this->getPHID();
+ if ($phid) {
+ return 'u.'.$phid;
+ }
+
+ return 'u.public';
+ }
+
+
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($prefs as $pref) {
$pref->delete();
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
'objectPHID = %s',
$this->getPHID());
foreach ($keys as $key) {
$key->delete();
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/'.$this->getID().'/panel/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorUserProfileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorUserTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
}
diff --git a/src/applications/phid/query/PhabricatorHandleQuery.php b/src/applications/phid/query/PhabricatorHandleQuery.php
index 2635b84298..1b72edb2e2 100644
--- a/src/applications/phid/query/PhabricatorHandleQuery.php
+++ b/src/applications/phid/query/PhabricatorHandleQuery.php
@@ -1,88 +1,89 @@
<?php
final class PhabricatorHandleQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $objectCapabilities;
private $phids = array();
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function requireObjectCapabilities(array $capabilities) {
$this->objectCapabilities = $capabilities;
return $this;
}
protected function getRequiredObjectCapabilities() {
if ($this->objectCapabilities) {
return $this->objectCapabilities;
}
return $this->getRequiredCapabilities();
}
protected function loadPage() {
$types = PhabricatorPHIDType::getAllTypes();
$phids = array_unique($this->phids);
if (!$phids) {
return array();
}
$object_query = id(new PhabricatorObjectQuery())
->withPHIDs($phids)
+ ->setParentQuery($this)
->requireCapabilities($this->getRequiredObjectCapabilities())
->setViewer($this->getViewer());
$objects = $object_query->execute();
$filtered = $object_query->getPolicyFilteredPHIDs();
$groups = array();
foreach ($phids as $phid) {
$type = phid_get_type($phid);
$groups[$type][] = $phid;
}
$results = array();
foreach ($groups as $type => $phid_group) {
$handles = array();
foreach ($phid_group as $key => $phid) {
if (isset($handles[$phid])) {
unset($phid_group[$key]);
// The input had a duplicate PHID; just skip it.
continue;
}
$handles[$phid] = id(new PhabricatorObjectHandle())
->setType($type)
->setPHID($phid);
if (isset($objects[$phid])) {
$handles[$phid]->setComplete(true);
} else if (isset($filtered[$phid])) {
$handles[$phid]->setPolicyFiltered(true);
}
}
if (isset($types[$type])) {
$type_objects = array_select_keys($objects, $phid_group);
if ($type_objects) {
$have_object_phids = array_keys($type_objects);
$types[$type]->loadHandles(
$this,
array_select_keys($handles, $have_object_phids),
$type_objects);
}
}
$results += $handles;
}
return $results;
}
public function getQueryApplicationClass() {
return null;
}
}
diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php
index 26d0668cc2..d4bcc9edf3 100644
--- a/src/applications/phid/query/PhabricatorObjectQuery.php
+++ b/src/applications/phid/query/PhabricatorObjectQuery.php
@@ -1,176 +1,181 @@
<?php
final class PhabricatorObjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $phids = array();
private $names = array();
private $types;
private $namedResults;
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
protected function loadPage() {
if ($this->namedResults === null) {
$this->namedResults = array();
}
$types = PhabricatorPHIDType::getAllTypes();
if ($this->types) {
$types = array_select_keys($types, $this->types);
}
$names = array_unique($this->names);
$phids = $this->phids;
// We allow objects to be named by their PHID in addition to their normal
// name so that, e.g., CLI tools which accept object names can also accept
// PHIDs and work as users expect.
$actually_phids = array();
if ($names) {
foreach ($names as $key => $name) {
if (!strncmp($name, 'PHID-', 5)) {
$actually_phids[] = $name;
$phids[] = $name;
unset($names[$key]);
}
}
}
$phids = array_unique($phids);
if ($names) {
$name_results = $this->loadObjectsByName($types, $names);
} else {
$name_results = array();
}
if ($phids) {
$phid_results = $this->loadObjectsByPHID($types, $phids);
} else {
$phid_results = array();
}
foreach ($actually_phids as $phid) {
if (isset($phid_results[$phid])) {
$name_results[$phid] = $phid_results[$phid];
}
}
$this->namedResults += $name_results;
return $phid_results + mpull($name_results, null, 'getPHID');
}
public function getNamedResults() {
if ($this->namedResults === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->namedResults;
}
private function loadObjectsByName(array $types, array $names) {
$groups = array();
foreach ($names as $name) {
foreach ($types as $type => $type_impl) {
if (!$type_impl->canLoadNamedObject($name)) {
continue;
}
$groups[$type][] = $name;
break;
}
}
$results = array();
foreach ($groups as $type => $group) {
$results += $types[$type]->loadNamedObjects($this, $group);
}
return $results;
}
private function loadObjectsByPHID(array $types, array $phids) {
$results = array();
- $workspace = $this->getObjectsFromWorkspace($phids);
-
- foreach ($phids as $key => $phid) {
- if (isset($workspace[$phid])) {
- $results[$phid] = $workspace[$phid];
- unset($phids[$key]);
- }
- }
-
- if (!$phids) {
- return $results;
- }
-
$groups = array();
foreach ($phids as $phid) {
$type = phid_get_type($phid);
$groups[$type][] = $phid;
}
$in_flight = $this->getPHIDsInFlight();
foreach ($groups as $type => $group) {
+ // We check the workspace for each group, because some groups may trigger
+ // other groups to load (for example, transactions load their objects).
+ $workspace = $this->getObjectsFromWorkspace($group);
+
+ foreach ($group as $key => $phid) {
+ if (isset($workspace[$phid])) {
+ $results[$phid] = $workspace[$phid];
+ unset($group[$key]);
+ }
+ }
+
+ if (!$group) {
+ continue;
+ }
+
// Don't try to load PHIDs which are already "in flight"; this prevents
// us from recursing indefinitely if policy checks or edges form a loop.
// We will decline to load the corresponding objects.
foreach ($group as $key => $phid) {
if (isset($in_flight[$phid])) {
unset($group[$key]);
}
}
if ($group && isset($types[$type])) {
$this->putPHIDsInFlight($group);
$objects = $types[$type]->loadObjects($this, $group);
- $results += mpull($objects, null, 'getPHID');
+
+ $map = mpull($objects, null, 'getPHID');
+ $this->putObjectsInWorkspace($map);
+ $results += $map;
}
}
return $results;
}
protected function didFilterResults(array $filtered) {
foreach ($this->namedResults as $name => $result) {
if (isset($filtered[$result->getPHID()])) {
unset($this->namedResults[$name]);
}
}
}
/**
* This query disables policy filtering if the only required capability is
* the view capability.
*
* The view capability is always checked in the subqueries, so we do not need
* to re-filter results. For any other set of required capabilities, we do.
*/
protected function shouldDisablePolicyFiltering() {
$view_capability = PhabricatorPolicyCapability::CAN_VIEW;
if ($this->getRequiredCapabilities() === array($view_capability)) {
return true;
}
return false;
}
public function getQueryApplicationClass() {
return null;
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index bbb0857315..e607ff2dc5 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,395 +1,401 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $slugs;
private $phrictionSlugs;
private $names;
private $nameTokens;
private $icons;
private $colors;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $needSlugs;
private $needMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withPhrictionSlugs(array $slugs) {
$this->phrictionSlugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'name' => $project->getName(),
);
}
protected function loadPage() {
$table = new PhabricatorProject();
$data = $this->loadStandardPageRows($table);
$projects = $table->loadAllFromArray($data);
if ($projects) {
$viewer_phid = $this->getViewer()->getPHID();
$project_phids = mpull($projects, 'getPHID');
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$need_edge_types = array();
if ($this->needMembers) {
$need_edge_types[] = $member_type;
} else {
foreach ($data as $row) {
$projects[$row['id']]->setIsUserMember(
$viewer_phid,
($row['viewerIsMember'] !== null));
}
}
if ($this->needWatchers) {
$need_edge_types[] = $watcher_type;
}
if ($need_edge_types) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes($need_edge_types)
->execute();
if ($this->needMembers) {
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachMemberPHIDs(
array_keys($edges[$phid][$member_type]));
$project->setIsUserMember(
$viewer_phid,
isset($edges[$phid][$member_type][$viewer_phid]));
}
}
if ($this->needWatchers) {
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachWatcherPHIDs(
array_keys($edges[$phid][$watcher_type]));
$project->setIsUserWatcher(
$viewer_phid,
isset($edges[$phid][$watcher_type][$viewer_phid]));
}
}
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
if ($this->needImages) {
$default = null;
$file_phids = mpull($projects, 'getProfileImagePHID');
- $files = id(new PhabricatorFileQuery())
- ->setParentQuery($this)
- ->setViewer($this->getViewer())
- ->withPHIDs($file_phids)
- ->execute();
- $files = mpull($files, null, 'getPHID');
+ $file_phids = array_filter($file_phids);
+ if ($file_phids) {
+ $files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
+ ->setViewer($this->getViewer())
+ ->withPHIDs($file_phids)
+ ->execute();
+ $files = mpull($files, null, 'getPHID');
+ } else {
+ $files = array();
+ }
+
foreach ($projects as $project) {
$file = idx($files, $project->getProfileImagePHID());
if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'project.png');
}
$file = $default;
}
$project->attachProfileImageFile($file);
}
}
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere(
'projectPHID IN (%Ls)',
mpull($projects, 'getPHID'));
$slugs = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slugs, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
return $projects;
}
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = parent::buildSelectClauseParts($conn);
// NOTE: Because visibility checks for projects depend on whether or not
// the user is a project member, we always load their membership. If we're
// loading all members anyway we can piggyback on that; otherwise we
// do an explicit join.
if (!$this->needMembers) {
$select[] = 'vm.dst viewerIsMember';
}
return $select;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->slugs);
}
if ($this->phrictionSlugs !== null) {
$where[] = qsprintf(
$conn,
'phrictionSlug IN (%Ls)',
$this->phrictionSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if (!$this->needMembers !== null) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST,
$this->getViewer()->getPHID());
}
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectProjectHasMemberEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
}
diff --git a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
index 96125b6aef..45d7bce1a6 100644
--- a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
+++ b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php
@@ -1,238 +1,238 @@
<?php
final class PhabricatorSpacesNamespaceQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
const KEY_ALL = 'spaces.all';
const KEY_DEFAULT = 'spaces.default';
const KEY_VIEWER = 'spaces.viewer';
private $ids;
private $phids;
private $isDefaultNamespace;
private $isArchived;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withIsDefaultNamespace($default) {
$this->isDefaultNamespace = $default;
return $this;
}
public function withIsArchived($archived) {
$this->isArchived = $archived;
return $this;
}
public function getQueryApplicationClass() {
return 'PhabricatorSpacesApplication';
}
protected function loadPage() {
return $this->loadStandardPage(new PhabricatorSpacesNamespace());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->isDefaultNamespace !== null) {
if ($this->isDefaultNamespace) {
$where[] = qsprintf(
$conn,
'isDefaultNamespace = 1');
} else {
$where[] = qsprintf(
$conn,
'isDefaultNamespace IS NULL');
}
}
if ($this->isArchived !== null) {
$where[] = qsprintf(
$conn,
'isArchived = %d',
(int)$this->isArchived);
}
return $where;
}
public static function destroySpacesCache() {
$cache = PhabricatorCaches::getRequestCache();
$cache->deleteKeys(
array(
self::KEY_ALL,
self::KEY_DEFAULT,
));
}
public static function getSpacesExist() {
return (bool)self::getAllSpaces();
}
public static function getViewerSpacesExist(PhabricatorUser $viewer) {
if (!self::getSpacesExist()) {
return false;
}
// If the viewer has access to only one space, pretend spaces simply don't
// exist.
$spaces = self::getViewerSpaces($viewer);
return (count($spaces) > 1);
}
public static function getAllSpaces() {
$cache = PhabricatorCaches::getRequestCache();
$cache_key = self::KEY_ALL;
$spaces = $cache->getKey($cache_key);
if ($spaces === null) {
$spaces = id(new PhabricatorSpacesNamespaceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->execute();
$spaces = mpull($spaces, null, 'getPHID');
$cache->setKey($cache_key, $spaces);
}
return $spaces;
}
public static function getDefaultSpace() {
$cache = PhabricatorCaches::getRequestCache();
$cache_key = self::KEY_DEFAULT;
$default_space = $cache->getKey($cache_key, false);
if ($default_space === false) {
$default_space = null;
$spaces = self::getAllSpaces();
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
$default_space = $space;
break;
}
}
$cache->setKey($cache_key, $default_space);
}
return $default_space;
}
public static function getViewerSpaces(PhabricatorUser $viewer) {
$cache = PhabricatorCaches::getRequestCache();
- $cache_key = self::KEY_VIEWER.'('.$viewer->getPHID().')';
+ $cache_key = self::KEY_VIEWER.'('.$viewer->getCacheFragment().')';
$result = $cache->getKey($cache_key);
if ($result === null) {
$spaces = self::getAllSpaces();
$result = array();
foreach ($spaces as $key => $space) {
$can_see = PhabricatorPolicyFilter::hasCapability(
$viewer,
$space,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_see) {
$result[$key] = $space;
}
}
$cache->setKey($cache_key, $result);
}
return $result;
}
public static function getViewerActiveSpaces(PhabricatorUser $viewer) {
$spaces = self::getViewerSpaces($viewer);
foreach ($spaces as $key => $space) {
if ($space->getIsArchived()) {
unset($spaces[$key]);
}
}
return $spaces;
}
public static function getSpaceOptionsForViewer(
PhabricatorUser $viewer,
$space_phid) {
$viewer_spaces = self::getViewerSpaces($viewer);
$map = array();
foreach ($viewer_spaces as $space) {
// Skip archived spaces, unless the object is already in that space.
if ($space->getIsArchived()) {
if ($space->getPHID() != $space_phid) {
continue;
}
}
$map[$space->getPHID()] = pht(
'Space %s: %s',
$space->getMonogram(),
$space->getNamespaceName());
}
asort($map);
return $map;
}
/**
* Get the Space PHID for an object, if one exists.
*
* This is intended to simplify performing a bunch of redundant checks; you
* can intentionally pass any value in (including `null`).
*
* @param wild
* @return phid|null
*/
public static function getObjectSpacePHID($object) {
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$space_phid = $object->getSpacePHID();
if ($space_phid === null) {
$default_space = self::getDefaultSpace();
if ($default_space) {
$space_phid = $default_space->getPHID();
}
}
return $space_phid;
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
index acc8f5ebe9..4519a1c902 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
@@ -1,139 +1,140 @@
<?php
final class PhabricatorSubscriptionsEditController
extends PhabricatorController {
private $phid;
private $action;
public function willProcessRequest(array $data) {
$this->phid = idx($data, 'phid');
$this->action = idx($data, 'action');
}
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
switch ($this->action) {
case 'add':
$is_add = true;
break;
case 'delete':
$is_add = false;
break;
default:
return new Aphront400Response();
}
$user = $request->getUser();
$phid = $this->phid;
$handle = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
if (phid_get_type($phid) == PhabricatorProjectProjectPHIDType::TYPECONST) {
// TODO: This is a big hack, but a weak argument for adding some kind
// of "load for role" feature to ObjectQuery, and also not a really great
// argument for adding some kind of "load extra stuff" feature to
// SubscriberInterface. Do this for now and wait for the best way forward
// to become more clear?
$object = id(new PhabricatorProjectQuery())
->setViewer($user)
->withPHIDs(array($phid))
->needWatchers(true)
->executeOne();
} else {
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($phid))
->executeOne();
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
return $this->buildErrorResponse(
pht('Bad Object'),
pht('This object is not subscribable.'),
$handle->getURI());
}
if ($object->isAutomaticallySubscribed($user->getPHID())) {
return $this->buildErrorResponse(
pht('Automatically Subscribed'),
pht('You are automatically subscribed to this object.'),
$handle->getURI());
}
if (!$object->shouldAllowSubscription($user->getPHID())) {
return $this->buildErrorResponse(
pht('You Can Not Subscribe'),
pht('You can not subscribe to this object.'),
$handle->getURI());
}
if ($object instanceof PhabricatorApplicationTransactionInterface) {
if ($is_add) {
$xaction_value = array(
'+' => array($user->getPHID()),
);
} else {
$xaction_value = array(
'-' => array($user->getPHID()),
);
}
$xaction = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue($xaction_value);
$editor = id($object->getApplicationTransactionEditor())
->setActor($user)
->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions(
$object->getApplicationTransactionObject(),
array($xaction));
} else {
// TODO: Eventually, get rid of this once everything implements
// PhabriatorApplicationTransactionInterface.
$editor = id(new PhabricatorSubscriptionsEditor())
->setActor($user)
->setObject($object);
if ($is_add) {
$editor->subscribeExplicit(array($user->getPHID()), $explicit = true);
} else {
$editor->unsubscribe(array($user->getPHID()));
}
$editor->save();
}
// TODO: We should just render the "Unsubscribe" action and swap it out
// in the document for Ajax requests.
return id(new AphrontReloadResponse())->setURI($handle->getURI());
}
private function buildErrorResponse($title, $message, $uri) {
$request = $this->getRequest();
$user = $request->getUser();
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle($title)
->appendChild($message)
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php
index d1bf5469f7..e1b3a158cd 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php
@@ -1,88 +1,99 @@
<?php
final class PhabricatorStandardCustomFieldRemarkup
extends PhabricatorStandardCustomField {
public function getFieldType() {
return 'remarkup';
}
public function renderEditControl(array $handles) {
return id(new PhabricatorRemarkupControl())
->setUser($this->getViewer())
->setLabel($this->getFieldName())
->setName($this->getFieldKey())
->setCaption($this->getCaption())
->setValue($this->getFieldValue());
}
public function getStyleForPropertyView() {
return 'block';
}
public function getApplicationTransactionRemarkupBlocks(
PhabricatorApplicationTransaction $xaction) {
return array(
$xaction->getNewValue(),
);
}
public function renderPropertyViewValue(array $handles) {
$value = $this->getFieldValue();
if (!strlen($value)) {
return null;
}
// TODO: Once this stabilizes, it would be nice to let fields batch this.
// For now, an extra query here and there on object detail pages isn't the
// end of the world.
$viewer = $this->getViewer();
return PhabricatorMarkupEngine::renderOneObject(
id(new PhabricatorMarkupOneOff())
->setContent($value)
->setPReserveLinebreaks(true),
'default',
$viewer);
}
public function getApplicationTransactionTitle(
PhabricatorApplicationTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
return pht(
'%s edited %s.',
$xaction->renderHandleLink($author_phid),
$this->getFieldName());
}
+ public function getApplicationTransactionTitleForFeed(
+ PhabricatorApplicationTransaction $xaction) {
+ $author_phid = $xaction->getAuthorPHID();
+ $object_phid = $xaction->getObjectPHID();
+ return pht(
+ '%s edited %s on %s.',
+ $xaction->renderHandleLink($author_phid),
+ $this->getFieldName(),
+ $xaction->renderHandleLink($object_phid));
+ }
+
public function getApplicationTransactionHasChangeDetails(
PhabricatorApplicationTransaction $xaction) {
return true;
}
public function getApplicationTransactionChangeDetails(
PhabricatorApplicationTransaction $xaction,
PhabricatorUser $viewer) {
return $xaction->renderTextCorpusChangeDetails(
$viewer,
$xaction->getOldValue(),
$xaction->getNewValue());
}
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,
);
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index 30b7b8dd0e..b3e9c08237 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,690 +1,697 @@
<?php
/**
* A @{class:PhabricatorQuery} which filters results according to visibility
* policies for the querying user. Broadly, this class allows you to implement
* a query that returns only objects the user is allowed to see.
*
* $results = id(new ExampleQuery())
* ->setViewer($user)
* ->withConstraint($example)
* ->execute();
*
* Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
* not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
* more practical interface for building usable queries against most object
* types.
*
* NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
* offset paging with policy filtering is not efficient. All results must be
* loaded into the application and filtered here: skipping `N` rows via offset
* is an `O(N)` operation with a large constant. Prefer cursor-based paging
* with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
* more efficiently in MySQL.
*
* @task config Query Configuration
* @task exec Executing Queries
* @task policyimpl Policy Query Implementation
*/
abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
private $viewer;
private $parentQuery;
private $rawResultLimit;
private $capabilities;
private $workspace = array();
private $inFlightPHIDs = array();
private $policyFilteredPHIDs = array();
/**
* Should we continue or throw an exception when a query result is filtered
* by policy rules?
*
* Values are `true` (raise exceptions), `false` (do not raise exceptions)
* and `null` (inherit from parent query, with no exceptions by default).
*/
private $raisePolicyExceptions;
/* -( Query Configuration )------------------------------------------------ */
/**
* Set the viewer who is executing the query. Results will be filtered
* according to the viewer's capabilities. You must set a viewer to execute
* a policy query.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task config
*/
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Get the query's viewer.
*
* @return PhabricatorUser The viewing user.
* @task config
*/
final public function getViewer() {
return $this->viewer;
}
/**
* Set the parent query of this query. This is useful for nested queries so
* that configuration like whether or not to raise policy exceptions is
* seamlessly passed along to child queries.
*
* @return this
* @task config
*/
final public function setParentQuery(PhabricatorPolicyAwareQuery $query) {
$this->parentQuery = $query;
return $this;
}
/**
* Get the parent query. See @{method:setParentQuery} for discussion.
*
* @return PhabricatorPolicyAwareQuery The parent query.
* @task config
*/
final public function getParentQuery() {
return $this->parentQuery;
}
/**
* Hook to configure whether this query should raise policy exceptions.
*
* @return this
* @task config
*/
final public function setRaisePolicyExceptions($bool) {
$this->raisePolicyExceptions = $bool;
return $this;
}
/**
* @return bool
* @task config
*/
final public function shouldRaisePolicyExceptions() {
return (bool)$this->raisePolicyExceptions;
}
/**
* @task config
*/
final public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query, expecting a single result. This method simplifies
* loading objects for detail pages or edit views.
*
* // Load one result by ID.
* $obj = id(new ExampleQuery())
* ->setViewer($user)
* ->withIDs(array($id))
* ->executeOne();
* if (!$obj) {
* return new Aphront404Response();
* }
*
* If zero results match the query, this method returns `null`.
* If one result matches the query, this method returns that result.
*
* If two or more results match the query, this method throws an exception.
* You should use this method only when the query constraints guarantee at
* most one match (e.g., selecting a specific ID or PHID).
*
* If one result matches the query but it is caught by the policy filter (for
* example, the user is trying to view or edit an object which exists but
* which they do not have permission to see) a policy exception is thrown.
*
* @return mixed Single result, or null.
* @task exec
*/
final public function executeOne() {
$this->setRaisePolicyExceptions(true);
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->setRaisePolicyExceptions(false);
throw $ex;
}
if (count($results) > 1) {
throw new Exception(pht('Expected a single result!'));
}
if (!$results) {
return null;
}
return head($results);
}
/**
* Execute the query, loading all visible results.
*
* @return list<PhabricatorPolicyInterface> Result objects.
* @task exec
*/
final public function execute() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
$parent_query = $this->getParentQuery();
if ($parent_query && ($this->raisePolicyExceptions === null)) {
$this->setRaisePolicyExceptions(
$parent_query->shouldRaisePolicyExceptions());
}
$results = array();
$filter = $this->getPolicyFilter();
$offset = (int)$this->getOffset();
$limit = (int)$this->getLimit();
$count = 0;
if ($limit) {
$need = $offset + $limit;
} else {
$need = 0;
}
$this->willExecute();
do {
if ($need) {
$this->rawResultLimit = min($need - $count, 1024);
} else {
$this->rawResultLimit = 0;
}
if ($this->canViewerUseQueryApplication()) {
try {
$page = $this->loadPage();
} catch (PhabricatorEmptyQueryException $ex) {
$page = array();
}
} else {
$page = array();
}
if ($page) {
$maybe_visible = $this->willFilterPage($page);
} else {
$maybe_visible = array();
}
if ($this->shouldDisablePolicyFiltering()) {
$visible = $maybe_visible;
} else {
$visible = $filter->apply($maybe_visible);
$policy_filtered = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$phid = $object->getPHID();
if ($phid) {
$policy_filtered[$phid] = $phid;
}
}
}
$this->addPolicyFilteredPHIDs($policy_filtered);
}
if ($visible) {
$this->putObjectsInWorkspace($this->getWorkspaceMapForPage($visible));
$visible = $this->didFilterPage($visible);
}
$removed = array();
foreach ($maybe_visible as $key => $object) {
if (empty($visible[$key])) {
$removed[$key] = $object;
}
}
$this->didFilterResults($removed);
foreach ($visible as $key => $result) {
++$count;
// If we have an offset, we just ignore that many results and start
// storing them only once we've hit the offset. This reduces memory
// requirements for large offsets, compared to storing them all and
// slicing them away later.
if ($count > $offset) {
$results[$key] = $result;
}
if ($need && ($count >= $need)) {
// If we have all the rows we need, break out of the paging query.
break 2;
}
}
if (!$this->rawResultLimit) {
// If we don't have a load count, we loaded all the results. We do
// not need to load another page.
break;
}
if (count($page) < $this->rawResultLimit) {
// If we have a load count but the unfiltered results contained fewer
// objects, we know this was the last page of objects; we do not need
// to load another page because we can deduce it would be empty.
break;
}
$this->nextPage($page);
} while (true);
$results = $this->didLoadResults($results);
return $results;
}
private function getPolicyFilter() {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
$capabilities = $this->getRequiredCapabilities();
$filter->requireCapabilities($capabilities);
$filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
return $filter;
}
protected function getRequiredCapabilities() {
if ($this->capabilities) {
return $this->capabilities;
}
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
protected function applyPolicyFilter(array $objects, array $capabilities) {
if ($this->shouldDisablePolicyFiltering()) {
return $objects;
}
$filter = $this->getPolicyFilter();
$filter->requireCapabilities($capabilities);
return $filter->apply($objects);
}
protected function didRejectResult(PhabricatorPolicyInterface $object) {
// Some objects (like commits) may be rejected because related objects
// (like repositories) can not be loaded. In some cases, we may need these
// related objects to determine the object policy, so it's expected that
// we may occasionally be unable to determine the policy.
try {
$policy = $object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW);
} catch (Exception $ex) {
$policy = null;
}
// Mark this object as filtered so handles can render "Restricted" instead
// of "Unknown".
$phid = $object->getPHID();
$this->addPolicyFilteredPHIDs(array($phid => $phid));
$this->getPolicyFilter()->rejectObject(
$object,
$policy,
PhabricatorPolicyCapability::CAN_VIEW);
}
public function addPolicyFilteredPHIDs(array $phids) {
$this->policyFilteredPHIDs += $phids;
if ($this->getParentQuery()) {
$this->getParentQuery()->addPolicyFilteredPHIDs($phids);
}
return $this;
}
/**
* Return a map of all object PHIDs which were loaded in the query but
* filtered out by policy constraints. This allows a caller to distinguish
* between objects which do not exist (or, at least, were filtered at the
* content level) and objects which exist but aren't visible.
*
* @return map<phid, phid> Map of object PHIDs which were filtered
* by policies.
* @task exec
*/
public function getPolicyFilteredPHIDs() {
return $this->policyFilteredPHIDs;
}
/* -( Query Workspace )---------------------------------------------------- */
/**
* Put a map of objects into the query workspace. Many queries perform
* subqueries, which can eventually end up loading the same objects more than
* once (often to perform policy checks).
*
* For example, loading a user may load the user's profile image, which might
* load the user object again in order to verify that the viewer has
* permission to see the file.
*
* The "query workspace" allows queries to load objects from elsewhere in a
* query block instead of refetching them.
*
* When using the query workspace, it's important to obey two rules:
*
* **Never put objects into the workspace which the viewer may not be able
* to see**. You need to apply all policy filtering //before// putting
* objects in the workspace. Otherwise, subqueries may read the objects and
* use them to permit access to content the user shouldn't be able to view.
*
* **Fully enrich objects pulled from the workspace.** After pulling objects
* from the workspace, you still need to load and attach any additional
- * content the query requests. Otherwise, a query might return objects without
- * requested content.
+ * content the query requests. Otherwise, a query might return objects
+ * without requested content.
*
* Generally, you do not need to update the workspace yourself: it is
* automatically populated as a side effect of objects surviving policy
* filtering.
*
* @param map<phid, PhabricatorPolicyInterface> Objects to add to the query
* workspace.
* @return this
* @task workspace
*/
public function putObjectsInWorkspace(array $objects) {
+ $parent = $this->getParentQuery();
+ if ($parent) {
+ $parent->putObjectsInWorkspace($objects);
+ return $this;
+ }
+
assert_instances_of($objects, 'PhabricatorPolicyInterface');
- $viewer_phid = $this->getViewer()->getPHID();
+ $viewer_fragment = $this->getViewer()->getCacheFragment();
// The workspace is scoped per viewer to prevent accidental contamination.
- if (empty($this->workspace[$viewer_phid])) {
- $this->workspace[$viewer_phid] = array();
+ if (empty($this->workspace[$viewer_fragment])) {
+ $this->workspace[$viewer_fragment] = array();
}
- $this->workspace[$viewer_phid] += $objects;
+ $this->workspace[$viewer_fragment] += $objects;
return $this;
}
/**
* Retrieve objects from the query workspace. For more discussion about the
* workspace mechanism, see @{method:putObjectsInWorkspace}. This method
* searches both the current query's workspace and the workspaces of parent
* queries.
*
* @param list<phid> List of PHIDs to retrieve.
* @return this
* @task workspace
*/
public function getObjectsFromWorkspace(array $phids) {
- $viewer_phid = $this->getViewer()->getPHID();
+ $parent = $this->getParentQuery();
+ if ($parent) {
+ return $parent->getObjectsFromWorkspace($phids);
+ }
+
+ $viewer_fragment = $this->getViewer()->getCacheFragment();
$results = array();
foreach ($phids as $key => $phid) {
- if (isset($this->workspace[$viewer_phid][$phid])) {
- $results[$phid] = $this->workspace[$viewer_phid][$phid];
+ if (isset($this->workspace[$viewer_fragment][$phid])) {
+ $results[$phid] = $this->workspace[$viewer_fragment][$phid];
unset($phids[$key]);
}
}
- if ($phids && $this->getParentQuery()) {
- $results += $this->getParentQuery()->getObjectsFromWorkspace($phids);
- }
-
return $results;
}
/**
* Convert a result page to a `<phid, PhabricatorPolicyInterface>` map.
*
* @param list<PhabricatorPolicyInterface> Objects.
* @return map<phid, PhabricatorPolicyInterface> Map of objects which can
* be put into the workspace.
* @task workspace
*/
protected function getWorkspaceMapForPage(array $results) {
$map = array();
foreach ($results as $result) {
$phid = $result->getPHID();
if ($phid !== null) {
$map[$phid] = $result;
}
}
return $map;
}
/**
* Mark PHIDs as in flight.
*
* PHIDs which are "in flight" are actively being queried for. Using this
* list can prevent infinite query loops by aborting queries which cycle.
*
* @param list<phid> List of PHIDs which are now in flight.
* @return this
*/
public function putPHIDsInFlight(array $phids) {
foreach ($phids as $phid) {
$this->inFlightPHIDs[$phid] = $phid;
}
return $this;
}
/**
* Get PHIDs which are currently in flight.
*
* PHIDs which are "in flight" are actively being queried for.
*
* @return map<phid, phid> PHIDs currently in flight.
*/
public function getPHIDsInFlight() {
$results = $this->inFlightPHIDs;
if ($this->getParentQuery()) {
$results += $this->getParentQuery()->getPHIDsInFlight();
}
return $results;
}
/* -( Policy Query Implementation )---------------------------------------- */
/**
* Get the number of results @{method:loadPage} should load. If the value is
* 0, @{method:loadPage} should load all available results.
*
* @return int The number of results to load, or 0 for all results.
* @task policyimpl
*/
final protected function getRawResultLimit() {
return $this->rawResultLimit;
}
/**
* Hook invoked before query execution. Generally, implementations should
* reset any internal cursors.
*
* @return void
* @task policyimpl
*/
protected function willExecute() {
return;
}
/**
* Load a raw page of results. Generally, implementations should load objects
* from the database. They should attempt to return the number of results
* hinted by @{method:getRawResultLimit}.
*
* @return list<PhabricatorPolicyInterface> List of filterable policy objects.
* @task policyimpl
*/
abstract protected function loadPage();
/**
* Update internal state so that the next call to @{method:loadPage} will
* return new results. Generally, you should adjust a cursor position based
* on the provided result page.
*
* @param list<PhabricatorPolicyInterface> The current page of results.
* @return void
* @task policyimpl
*/
abstract protected function nextPage(array $page);
/**
* Hook for applying a page filter prior to the privacy filter. This allows
* you to drop some items from the result set without creating problems with
* pagination or cursor updates. You can also load and attach data which is
* required to perform policy filtering.
*
* Generally, you should load non-policy data and perform non-policy filtering
* later, in @{method:didFilterPage}. Strictly fewer objects will make it that
* far (so the program will load less data) and subqueries from that context
* can use the query workspace to further reduce query load.
*
* This method will only be called if data is available. Implementations
* do not need to handle the case of no results specially.
*
* @param list<wild> Results from `loadPage()`.
* @return list<PhabricatorPolicyInterface> Objects for policy filtering.
* @task policyimpl
*/
protected function willFilterPage(array $page) {
return $page;
}
/**
* Hook for performing additional non-policy loading or filtering after an
* object has satisfied all policy checks. Generally, this means loading and
* attaching related data.
*
* Subqueries executed during this phase can use the query workspace, which
* may improve performance or make circular policies resolvable. Data which
* is not necessary for policy filtering should generally be loaded here.
*
* This callback can still filter objects (for example, if attachable data
* is discovered to not exist), but should not do so for policy reasons.
*
* This method will only be called if data is available. Implementations do
* not need to handle the case of no results specially.
*
* @param list<wild> Results from @{method:willFilterPage()}.
* @return list<PhabricatorPolicyInterface> Objects after additional
* non-policy processing.
*/
protected function didFilterPage(array $page) {
return $page;
}
/**
* Hook for removing filtered results from alternate result sets. This
* hook will be called with any objects which were returned by the query but
* filtered for policy reasons. The query should remove them from any cached
* or partial result sets.
*
* @param list<wild> List of objects that should not be returned by alternate
* result mechanisms.
* @return void
* @task policyimpl
*/
protected function didFilterResults(array $results) {
return;
}
/**
* Hook for applying final adjustments before results are returned. This is
* used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
* that are queried during reverse paging.
*
* @param list<PhabricatorPolicyInterface> Query results.
* @return list<PhabricatorPolicyInterface> Final results.
* @task policyimpl
*/
protected function didLoadResults(array $results) {
return $results;
}
/**
* Allows a subclass to disable policy filtering. This method is dangerous.
* It should be used only if the query loads data which has already been
* filtered (for example, because it wraps some other query which uses
* normal policy filtering).
*
* @return bool True to disable all policy filtering.
* @task policyimpl
*/
protected function shouldDisablePolicyFiltering() {
return false;
}
/**
* If this query belongs to an application, return the application class name
* here. This will prevent the query from returning results if the viewer can
* not access the application.
*
* If this query does not belong to an application, return `null`.
*
* @return string|null Application class name.
*/
abstract public function getQueryApplicationClass();
/**
* Determine if the viewer has permission to use this query's application.
* For queries which aren't part of an application, this method always returns
* true.
*
* @return bool True if the viewer has application-level permission to
* execute the query.
*/
public function canViewerUseQueryApplication() {
$class = $this->getQueryApplicationClass();
if (!$class) {
return true;
}
$viewer = $this->getViewer();
return PhabricatorApplication::isClassInstalledForViewer($class, $viewer);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 11:42 PM (23 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
141360
Default Alt Text
(108 KB)

Event Timeline