Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2891355
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
94 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php
index fd859a8fbe..28393b4502 100644
--- a/src/applications/files/query/PhabricatorFileQuery.php
+++ b/src/applications/files/query/PhabricatorFileQuery.php
@@ -1,236 +1,238 @@
<?php
/**
* @group file
*/
final class PhabricatorFileQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $explicitUploads;
private $transforms;
private $dateCreatedAfter;
private $dateCreatedBefore;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
/**
* Select files which are transformations of some other file. For example,
* you can use this query to find previously generated thumbnails of an image
* file.
*
* As a parameter, provide a list of transformation specifications. Each
* specification is a dictionary with the keys `originalPHID` and `transform`.
* The `originalPHID` is the PHID of the original file (the file which was
* transformed) and the `transform` is the name of the transform to query
* for. If you pass `true` as the `transform`, all transformations of the
* file will be selected.
*
* For example:
*
* array(
* array(
* 'originalPHID' => 'PHID-FILE-aaaa',
* 'transform' => 'sepia',
* ),
* array(
* 'originalPHID' => 'PHID-FILE-bbbb',
* 'transform' => true,
* ),
* )
*
* This selects the `"sepia"` transformation of the file with PHID
* `PHID-FILE-aaaa` and all transformations of the file with PHID
* `PHID-FILE-bbbb`.
*
* @param list<dict> List of transform specifications, described above.
* @return this
*/
public function withTransforms(array $specs) {
foreach ($specs as $spec) {
if (!is_array($spec) ||
empty($spec['originalPHID']) ||
empty($spec['transform'])) {
throw new Exception(
"Transform specification must be a dictionary with keys ".
"'originalPHID' and 'transform'!");
}
}
$this->transforms = $specs;
return $this;
}
public function showOnlyExplicitUploads($explicit_uploads) {
$this->explicitUploads = $explicit_uploads;
return $this;
}
protected function loadPage() {
$table = new PhabricatorFile();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT f.* FROM %T f %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$files = $table->loadAllFromArray($data);
if (!$files) {
return $files;
}
// We need to load attached objects to perform policy checks for files.
// First, load the edges.
$edge_type = PhabricatorEdgeConfig::TYPE_FILE_HAS_OBJECT;
$phids = mpull($files, 'getPHID');
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(array($edge_type))
->execute();
$object_phids = array();
foreach ($files as $file) {
$phids = array_keys($edges[$file->getPHID()][$edge_type]);
$file->attachObjectPHIDs($phids);
foreach ($phids as $phid) {
$object_phids[$phid] = true;
}
}
+ $object_phids = array_keys($object_phids);
// Now, load the objects.
$objects = array();
if ($object_phids) {
$objects = id(new PhabricatorObjectQuery())
+ ->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($object_phids)
->execute();
$objects = mpull($objects, null, 'getPHID');
}
foreach ($files as $file) {
$file_objects = array_select_keys($objects, $file->getObjectPHIDs());
$file->attachObjects($file_objects);
}
return $files;
}
private function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
if ($this->transforms) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T t ON t.transformedPHID = f.phid',
id(new PhabricatorTransformedFile())->getTableName());
}
return implode(' ', $joins);
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'f.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'f.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'f.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->explicitUploads) {
$where[] = qsprintf(
$conn_r,
'f.isExplicitUpload = true');
}
if ($this->transforms) {
$clauses = array();
foreach ($this->transforms as $transform) {
if ($transform['transform'] === true) {
$clauses[] = qsprintf(
$conn_r,
'(t.originalPHID = %s)',
$transform['originalPHID']);
} else {
$clauses[] = qsprintf(
$conn_r,
'(t.originalPHID = %s AND t.transform = %s)',
$transform['originalPHID'],
$transform['transform']);
}
}
$where[] = qsprintf($conn_r, '(%Q)', implode(') OR (', $clauses));
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn_r,
'f.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn_r,
'f.dateCreated <= %d',
$this->dateCreatedBefore);
}
return $this->formatWhereClause($where);
}
protected function getPagingColumn() {
return 'f.id';
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 51e2a55fca..37bd60af16 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,948 +1,950 @@
<?php
/**
* @group file
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorPolicyInterface {
const STORAGE_FORMAT_RAW = 'raw';
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorFilePHIDTypeFile::TYPECONST);
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception("No file was uploaded!");
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception("File is not an uploaded file.");
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
throw new Exception("File size disagrees with uploaded size.");
}
self::validateFileSize(strlen($file_data));
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
self::validateFileSize(strlen($data));
return self::newFromFileData($data, $params);
}
private static function validateFileSize($size) {
$limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
if (!$limit) {
return;
}
$limit = phabricator_parse_bytes($limit);
if ($size > $limit) {
throw new PhabricatorFileUploadException(-1000);
}
}
/**
* Given a block of data, try to load an existing file with the same content
* if one exists. If it does not, build a new file.
*
* This method is generally used when we have some piece of semi-trusted data
* like a diff or a file from a repository that we want to show to the user.
* We can't just dump it out because it may be dangerous for any number of
* reasons; instead, we need to serve it through the File abstraction so it
* ends up on the CDN domain if one is configured and so on. However, if we
* simply wrote a new file every time we'd potentially end up with a lot
* of redundant data in file storage.
*
* To solve these problems, we use file storage as a cache and reuse the
* same file again if we've previously written it.
*
* NOTE: This method unguards writes.
*
* @param string Raw file data.
* @param dict Dictionary of file information.
*/
public static function buildFromFileDataOrHash(
$data,
array $params = array()) {
$file = id(new PhabricatorFile())->loadOneWhere(
'name = %s AND contentHash = %s LIMIT 1',
self::normalizeFileName(idx($params, 'name')),
self::hashFileContent($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFileFromContentHash($hash, $params) {
// Check to see if a file with same contentHash exist
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1', $hash);
if ($file) {
// copy storageEngine, storageHandle, storageFormat
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_byteSize = $file->getByteSize();
$copy_of_mimeType = $file->getMimeType();
$file_name = idx($params, 'name');
$file_name = self::normalizeFileName($file_name);
$file_ttl = idx($params, 'ttl');
$authorPHID = idx($params, 'authorPHID');
$new_file = new PhabricatorFile();
$new_file->setName($file_name);
$new_file->setByteSize($copy_of_byteSize);
$new_file->setAuthorPHID($authorPHID);
$new_file->setTtl($file_ttl);
$new_file->setContentHash($hash);
$new_file->setStorageEngine($copy_of_storage_engine);
$new_file->setStorageHandle($copy_of_storage_handle);
$new_file->setStorageFormat($copy_of_storage_format);
$new_file->setMimeType($copy_of_mimeType);
$new_file->copyDimensions($file);
$new_file->save();
return $new_file;
}
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
$selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector');
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$selector = PhabricatorEnv::newObjectFromConfig(
'storage.engine-selector');
$engines = $selector->selectStorageEngines($data, $params);
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception("No valid storage engines are available!");
}
$file = new PhabricatorFile();
$data_handle = null;
$engine_identifier = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$engine,
$data,
$params);
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
"All storage engines failed to write file:",
$exceptions);
}
$file_name = idx($params, 'name');
$file_name = self::normalizeFileName($file_name);
$file_ttl = idx($params, 'ttl');
// If for whatever reason, authorPHID isn't passed as a param
// (always the case with newFromFileDownload()), store a ''
$authorPHID = idx($params, 'authorPHID');
$file->setName($file_name);
$file->setByteSize(strlen($data));
$file->setAuthorPHID($authorPHID);
$file->setTtl($file_ttl);
$file->setContentHash(self::hashFileContent($data));
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
// TODO: This is probably YAGNI, but allows for us to do encryption or
// compression later if we want.
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->setIsExplicitUpload(idx($params, 'isExplicitUpload') ? 1 : 0);
if (isset($params['mime-type'])) {
$file->setMimeType($params['mime-type']);
} else {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing
}
$file->save();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
"You can not migrate a file which hasn't yet been saved.");
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->save();
$old_engine->deleteFile($old_handle);
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$data_handle = $engine->writeFile($data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' executed writeFile() but did ".
"not return a valid handle ('{$data_handle}') to the data: it ".
"must be nonempty and no longer than 255 characters.");
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' returned an improper engine ".
"identifier '{$engine_identifier}': it must be nonempty ".
"and no longer than 32 characters.");
}
return array($engine_identifier, $data_handle);
}
public static function newFromFileDownload($uri, array $params = array()) {
// Make sure we're allowed to make a request first
if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
throw new Exception("Outbound HTTP requests are disabled!");
}
$uri = new PhutilURI($uri);
$protocol = $uri->getProtocol();
switch ($protocol) {
case 'http':
case 'https':
break;
default:
// Make sure we are not accessing any file:// URIs or similar.
return null;
}
$timeout = 5;
list($file_data) = id(new HTTPSFuture($uri))
->setTimeout($timeout)
->resolvex();
$params = $params + array(
'name' => basename($uri),
);
return self::newFromFileData($file_data, $params);
}
public static function normalizeFileName($file_name) {
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
$file_name = preg_replace($pattern, '_', $file_name);
$file_name = preg_replace('@_+@', '_', $file_name);
$file_name = trim($file_name, '_');
$disallowed_filenames = array(
'.' => 'dot',
'..' => 'dotdot',
'' => 'file',
);
$file_name = idx($disallowed_filenames, $file_name, $file_name);
return $file_name;
}
public function delete() {
// We want to delete all the rows which mark this file as the transformation
// of some other file (since we're getting rid of it). We also delete all
// the transformations of this file, so that a user who deletes an image
// doesn't need to separately hunt down and delete a bunch of thumbnails and
// resizes of it.
$outbound_xforms = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms(
array(
array(
'originalPHID' => $this->getPHID(),
'transform' => true,
),
))
->execute();
foreach ($outbound_xforms as $outbound_xform) {
$outbound_xform->delete();
}
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$this->getPHID());
$this->openTransaction();
foreach ($inbound_xforms as $inbound_xform) {
$inbound_xform->delete();
}
$ret = parent::delete();
$this->saveTransaction();
// Check to see if other files are using storage
$other_file = id(new PhabricatorFile())->loadAllWhere(
'storageEngine = %s AND storageHandle = %s AND
storageFormat = %s AND id != %d LIMIT 1',
$this->getStorageEngine(),
$this->getStorageHandle(),
$this->getStorageFormat(),
$this->getID());
// If this is the only file using the storage, delete storage
if (!$other_file) {
$engine = $this->instantiateStorageEngine();
try {
$engine->deleteFile($this->getStorageHandle());
} catch (Exception $ex) {
// In the worst case, we're leaving some data stranded in a storage
// engine, which is fine.
phlog($ex);
}
}
return $ret;
}
public static function hashFileContent($data) {
return sha1($data);
}
public function loadFileData() {
$engine = $this->instantiateStorageEngine();
$data = $engine->readFile($this->getStorageHandle());
switch ($this->getStorageFormat()) {
case self::STORAGE_FORMAT_RAW:
$data = $data;
break;
default:
throw new Exception("Unknown storage format.");
}
return $data;
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
"You must save a file before you can generate a view URI.");
}
$name = phutil_escape_uri($this->getName());
$path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
return PhabricatorEnv::getCDNURI($path);
}
public function getInfoURI() {
return '/file/info/'.$this->getPHID().'/';
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string) $uri;
}
public function getProfileThumbURI() {
$path = '/file/xform/thumb-profile/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb60x45URI() {
$path = '/file/xform/thumb-60x45/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb160x120URI() {
$path = '/file/xform/thumb-160x120/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getPreview140URI() {
$path = '/file/xform/preview-140/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getPreview220URI() {
$path = '/file/xform/preview-220/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb220x165URI() {
$path = '/file/xform/thumb-220x165/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb280x210URI() {
$path = '/file/xform/thumb-280x210/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isAudio() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
throw new Exception('Unknown type matched as image MIME type.');
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
protected function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
"Storage engine '{$engine_identifier}' could not be located!");
}
public static function buildAllEngines() {
$engines = id(new PhutilSymbolLoader())
->setType('class')
->setConcreteOnly(true)
->setAncestorClass('PhabricatorFileStorageEngine')
->selectAndLoadSymbols();
$results = array();
foreach ($engines as $engine_class) {
$results[] = newv($engine_class['name'], array());
}
return $results;
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function getDisplayIconForMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type, 'docs_file');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
throw new Exception(
"This file is not a viewable image.");
}
if (!function_exists("imagecreatefromstring")) {
throw new Exception(
"Cannot retrieve image information.");
}
$data = $this->loadFileData();
$img = imagecreatefromstring($data);
if ($img === false) {
throw new Exception(
"Error when decoding image.");
}
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
if ($save) {
$this->save();
}
return $this;
}
public function copyDimensions(PhabricatorFile $file) {
$metadata = $file->getMetadata();
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
if ($width) {
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
}
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
if ($height) {
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
}
return $this;
}
public static function getMetadataName($metadata) {
switch ($metadata) {
case self::METADATA_IMAGE_WIDTH:
$name = pht('Width');
break;
case self::METADATA_IMAGE_HEIGHT:
$name = pht('Height');
break;
default:
$name = ucfirst($metadata);
break;
}
return $name;
}
/**
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
* resources. The builtin mechanism allows files shipped with Phabricator
* to be treated like normal files so that APIs do not need to special case
* things like default images or deleted files.
*
* Builtins are located in `resources/builtin/` and identified by their
* name.
*
* @param PhabricatorUser Viewing user.
* @param list<string> List of builtin file names.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $names) {
$specs = array();
foreach ($names as $name) {
$specs[] = array(
'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
'transform' => 'builtin:'.$name,
);
}
+ // NOTE: Anyone is allowed to access builtin files.
+
$files = id(new PhabricatorFileQuery())
- ->setViewer($user)
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms($specs)
->execute();
$files = mpull($files, null, 'getName');
$root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/';
$build = array();
foreach ($names as $name) {
if (isset($files[$name])) {
continue;
}
// This is just a sanity check to prevent loading arbitrary files.
if (basename($name) != $name) {
throw new Exception("Invalid builtin name '{$name}'!");
}
$path = $root.$name;
if (!Filesystem::pathExists($path)) {
throw new Exception("Builtin '{$path}' does not exist!");
}
$data = Filesystem::readFile($path);
$params = array(
'name' => $name,
'ttl' => time() + (60 * 60 * 24 * 7),
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData($data, $params);
$xform = id(new PhabricatorTransformedFile())
->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
->setTransform('builtin:'.$name)
->setTransformedPHID($file->getPHID())
->save();
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$files[$name] = $file;
}
return $files;
}
/**
* Convenience wrapper for @{method:loadBuiltins}.
*
* @param PhabricatorUser Viewing user.
* @param string Single builtin name to load.
* @return PhabricatorFile Corresponding builtin file.
*/
public static function loadBuiltin(PhabricatorUser $user, $name) {
return idx(self::loadBuiltins($user, array($name)), $name);
}
public function getObjects() {
return $this->assertAttached($this->objects);
}
public function attachObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjectPHIDs() {
return $this->assertAttached($this->objectPHIDs);
}
public function attachObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function getImageHeight() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
}
public function getImageWidth() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
}
/**
* Write the policy edge between this file and some object.
*
* @param PhabricatorUser Acting user.
* @param phid Object PHID to attach to.
* @return this
*/
public function attachToObject(PhabricatorUser $actor, $phid) {
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
id(new PhabricatorEdgeEditor())
->setActor($actor)
->setSuppressEvents(true)
->addEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// TODO: Implement proper per-object policies.
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if ($this->getAuthorPHID() == $viewer_phid) {
return true;
}
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If you can see any object this file is attached to, you can see
// the file.
return (count($this->getObjects()) > 0);
}
return false;
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who uploaded a file can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'Files attached to objects are visible to users who can view '.
'those objects.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
}
diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php
index aa1a22fcdc..da7dbd7b83 100644
--- a/src/applications/macro/query/PhabricatorMacroQuery.php
+++ b/src/applications/macro/query/PhabricatorMacroQuery.php
@@ -1,218 +1,219 @@
<?php
/**
* @group phriction
*/
final class PhabricatorMacroQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authors;
private $names;
private $nameLike;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $flagColor;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_ACTIVE = 'status-active';
const STATUS_DISABLED = 'status-disabled';
public static function getStatusOptions() {
return array(
self::STATUS_ACTIVE => pht('Active Macros'),
self::STATUS_DISABLED => pht('Disabled Macros'),
self::STATUS_ANY => pht('Active and Disabled Macros'),
);
}
public static function getFlagColorsOptions() {
$options = array('-1' => pht('(No Filtering)'));
foreach (PhabricatorFlagColor::getColorNameMap() as $color => $name) {
$options[$color] = $name;
}
return $options;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $authors) {
$this->authors = $authors;
return $this;
}
public function withNameLike($name) {
$this->nameLike = $name;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withFlagColor($flag_color) {
$this->flagColor = $flag_color;
return $this;
}
protected function loadPage() {
$macro_table = new PhabricatorFileImageMacro();
$conn = $macro_table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT m.* FROM %T m %Q %Q %Q',
$macro_table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $macro_table->loadAllFromArray($rows);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn,
'm.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
$this->phids);
}
if ($this->authors) {
$where[] = qsprintf(
$conn,
'm.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->nameLike) {
$where[] = qsprintf(
$conn,
'm.name LIKE %~',
$this->nameLike);
}
if ($this->names) {
$where[] = qsprintf(
$conn,
'm.name IN (%Ls)',
$this->names);
}
switch ($this->status) {
case self::STATUS_ACTIVE:
$where[] = qsprintf(
$conn,
'm.isDisabled = 0');
break;
case self::STATUS_DISABLED:
$where[] = qsprintf(
$conn,
'm.isDisabled = 1');
break;
case self::STATUS_ANY:
break;
default:
throw new Exception("Unknown status '{$this->status}'!");
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'm.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'm.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->flagColor != '-1' && $this->flagColor !== null) {
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($this->getViewer()->getPHID()))
->withTypes(array(PhabricatorMacroPHIDTypeMacro::TYPECONST))
->withColors(array($this->flagColor))
->setViewer($this->getViewer())
->execute();
if (empty($flags)) {
throw new PhabricatorEmptyQueryException('No matching flags.');
} else {
$where[] = qsprintf(
$conn,
'm.phid IN (%Ls)',
mpull($flags, 'getObjectPHID'));
}
}
$where[] = $this->buildPagingClause($conn);
return $this->formatWhereClause($where);
}
- protected function willFilterPage(array $macros) {
+ protected function didFilterPage(array $macros) {
$file_phids = mpull($macros, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
+ ->setParentQuery($this)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($macros as $key => $macro) {
$file = idx($files, $macro->getFilePHID());
if (!$file) {
unset($macros[$key]);
continue;
}
$macro->attachFile($file);
}
return $macros;
}
protected function getPagingColumn() {
return 'm.id';
}
}
diff --git a/src/applications/paste/query/PhabricatorPasteQuery.php b/src/applications/paste/query/PhabricatorPasteQuery.php
index d05f78b457..101ceb84ed 100644
--- a/src/applications/paste/query/PhabricatorPasteQuery.php
+++ b/src/applications/paste/query/PhabricatorPasteQuery.php
@@ -1,244 +1,245 @@
<?php
/**
* @group paste
*/
final class PhabricatorPasteQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $parentPHIDs;
private $needContent;
private $needRawContent;
private $languages;
private $includeNoLanguage;
private $dateCreatedAfter;
private $dateCreatedBefore;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withParentPHIDs(array $phids) {
$this->parentPHIDs = $phids;
return $this;
}
public function needContent($need_content) {
$this->needContent = $need_content;
return $this;
}
public function needRawContent($need_raw_content) {
$this->needRawContent = $need_raw_content;
return $this;
}
public function withLanguages(array $languages) {
$this->includeNoLanguage = false;
foreach ($languages as $key => $language) {
if ($language === null) {
$languages[$key] = '';
continue;
}
}
$this->languages = $languages;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
protected function loadPage() {
$table = new PhabricatorPaste();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT paste.* FROM %T paste %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$pastes = $table->loadAllFromArray($data);
return $pastes;
}
- protected function willFilterPage(array $pastes) {
+ protected function didFilterPage(array $pastes) {
if ($this->needRawContent) {
$pastes = $this->loadRawContent($pastes);
}
if ($this->needContent) {
$pastes = $this->loadContent($pastes);
}
return $pastes;
}
protected function buildWhereClause($conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->parentPHIDs) {
$where[] = qsprintf(
$conn_r,
'parentPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->languages) {
$where[] = qsprintf(
$conn_r,
'language IN (%Ls)',
$this->languages);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn_r,
'dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn_r,
'dateCreated <= %d',
$this->dateCreatedBefore);
}
return $this->formatWhereClause($where);
}
private function getContentCacheKey(PhabricatorPaste $paste) {
return 'P'.$paste->getID().':content/'.$paste->getLanguage();
}
private function loadRawContent(array $pastes) {
$file_phids = mpull($pastes, 'getFilePHID');
$files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($pastes as $key => $paste) {
$file = idx($files, $paste->getFilePHID());
if (!$file) {
unset($pastes[$key]);
continue;
}
$paste->attachRawContent($file->loadFileData());
}
return $pastes;
}
private function loadContent(array $pastes) {
$cache = new PhabricatorKeyValueDatabaseCache();
$cache = new PhutilKeyValueCacheProfiler($cache);
$cache->setProfiler(PhutilServiceProfiler::getInstance());
$keys = array();
foreach ($pastes as $paste) {
$keys[] = $this->getContentCacheKey($paste);
}
$caches = $cache->getKeys($keys);
$results = array();
$need_raw = array();
foreach ($pastes as $key => $paste) {
$key = $this->getContentCacheKey($paste);
if (isset($caches[$key])) {
$paste->attachContent(phutil_safe_html($caches[$key]));
$results[$paste->getID()] = $paste;
} else {
$need_raw[$key] = $paste;
}
}
if (!$need_raw) {
return $results;
}
$write_data = array();
$need_raw = $this->loadRawContent($need_raw);
foreach ($need_raw as $key => $paste) {
$content = $this->buildContent($paste);
$paste->attachContent($content);
$write_data[$this->getContentCacheKey($paste)] = (string)$content;
$results[$paste->getID()] = $paste;
}
$cache->setKeys($write_data);
return $results;
}
private function buildContent(PhabricatorPaste $paste) {
$language = $paste->getLanguage();
$source = $paste->getRawContent();
if (empty($language)) {
return PhabricatorSyntaxHighlighter::highlightWithFilename(
$paste->getTitle(),
$source);
} else {
return PhabricatorSyntaxHighlighter::highlightWithLanguage(
$language,
$source);
}
}
}
diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php
index 1f0c12bfc0..39613c5011 100644
--- a/src/applications/people/query/PhabricatorPeopleQuery.php
+++ b/src/applications/people/query/PhabricatorPeopleQuery.php
@@ -1,282 +1,285 @@
<?php
final class PhabricatorPeopleQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $usernames;
private $realnames;
private $emails;
private $phids;
private $ids;
private $dateCreatedAfter;
private $dateCreatedBefore;
private $isAdmin;
private $isSystemAgent;
private $isDisabled;
private $nameLike;
private $needPrimaryEmail;
private $needProfile;
private $needProfileImage;
private $needStatus;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withEmails(array $emails) {
$this->emails = $emails;
return $this;
}
public function withRealnames(array $realnames) {
$this->realnames = $realnames;
return $this;
}
public function withUsernames(array $usernames) {
$this->usernames = $usernames;
return $this;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withIsAdmin($admin) {
$this->isAdmin = $admin;
return $this;
}
public function withIsSystemAgent($system_agent) {
$this->isSystemAgent = $system_agent;
return $this;
}
public function withIsDisabled($disabled) {
$this->isDisabled = $disabled;
return $this;
}
public function withNameLike($like) {
$this->nameLike = $like;
return $this;
}
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
}
public function needProfile($need) {
$this->needProfile = $need;
return $this;
}
public function needProfileImage($need) {
$this->needProfileImage = $need;
return $this;
}
public function needStatus($need) {
$this->needStatus = $need;
return $this;
}
public function loadPage() {
$table = new PhabricatorUser();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T user %Q %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinsClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildApplicationSearchGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
if ($this->needPrimaryEmail) {
$table->putInSet(new LiskDAOSet());
}
- $users = $table->loadAllFromArray($data);
+ return $table->loadAllFromArray($data);
+ }
+ protected function didFilterPage(array $users) {
if ($this->needProfile) {
$user_list = mpull($users, null, 'getPHID');
$profiles = new PhabricatorUserProfile();
$profiles = $profiles->loadAllWhere('userPHID IN (%Ls)',
array_keys($user_list));
$profiles = mpull($profiles, null, 'getUserPHID');
foreach ($user_list as $user_phid => $user) {
$profile = idx($profiles, $user_phid);
if (!$profile) {
$profile = new PhabricatorUserProfile();
$profile->setUserPHID($user_phid);
}
$user->attachUserProfile($profile);
}
}
if ($this->needProfileImage) {
$user_profile_file_phids = mpull($users, 'getProfileImagePHID');
$user_profile_file_phids = array_filter($user_profile_file_phids);
if ($user_profile_file_phids) {
$files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($user_profile_file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
foreach ($users as $user) {
$image_phid = $user->getProfileImagePHID();
if (isset($files[$image_phid])) {
$profile_image_uri = $files[$image_phid]->getBestURI();
} else {
$profile_image_uri = PhabricatorUser::getDefaultProfileImageURI();
}
$user->attachProfileImageURI($profile_image_uri);
}
}
if ($this->needStatus) {
$user_list = mpull($users, null, 'getPHID');
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
array_keys($user_list));
foreach ($user_list as $phid => $user) {
$status = idx($statuses, $phid);
if ($status) {
$user->attachStatus($status);
}
}
}
return $users;
}
private function buildJoinsClause($conn_r) {
$joins = array();
if ($this->emails) {
$email_table = new PhabricatorUserEmail();
$joins[] = qsprintf(
$conn_r,
'JOIN %T email ON email.userPHID = user.PHID',
$email_table->getTableName());
}
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
$joins = implode(' ', $joins);
return $joins;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->usernames) {
$where[] = qsprintf(
$conn_r,
'user.userName IN (%Ls)',
$this->usernames);
}
if ($this->emails) {
$where[] = qsprintf(
$conn_r,
'email.address IN (%Ls)',
$this->emails);
}
if ($this->realnames) {
$where[] = qsprintf(
$conn_r,
'user.realName IN (%Ls)',
$this->realnames);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'user.phid IN (%Ls)',
$this->phids);
}
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'user.id IN (%Ld)',
$this->ids);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn_r,
'user.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn_r,
'user.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->isAdmin) {
$where[] = qsprintf(
$conn_r,
'user.isAdmin = 1');
}
if ($this->isDisabled) {
$where[] = qsprintf(
$conn_r,
'user.isDisabled = 1');
}
if ($this->isSystemAgent) {
$where[] = qsprintf(
$conn_r,
'user.isSystemAgent = 1');
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn_r,
'user.username LIKE %~ OR user.realname LIKE %~',
$this->nameLike,
$this->nameLike);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
protected function getPagingColumn() {
return 'user.id';
}
protected function getApplicationSearchObjectPHIDColumn() {
return 'user.phid';
}
}
diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php
index 1e492fe5d9..a6ffa02e7e 100644
--- a/src/applications/phid/query/PhabricatorObjectQuery.php
+++ b/src/applications/phid/query/PhabricatorObjectQuery.php
@@ -1,141 +1,155 @@
<?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;
}
public 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 Exception("Call execute() before getNamedResults()!");
}
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;
}
- $results = array();
foreach ($groups as $type => $group) {
if (isset($types[$type])) {
$objects = $types[$type]->loadObjects($this, $group);
$results += mpull($objects, null, 'getPHID');
}
}
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 because it is performed in the
* subqueries which actually load objects. We don't need to re-filter
* results, since policies have already been applied.
*/
protected function shouldDisablePolicyFiltering() {
return true;
}
}
diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php
index b619e25739..71bdd3ab0e 100644
--- a/src/applications/pholio/query/PholioImageQuery.php
+++ b/src/applications/pholio/query/PholioImageQuery.php
@@ -1,157 +1,164 @@
<?php
/**
* @group pholio
*/
final class PholioImageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $mockIDs;
private $obsolete;
private $needInlineComments;
private $mockCache = array();
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withMockIDs(array $mock_ids) {
$this->mockIDs = $mock_ids;
return $this;
}
public function withObsolete($obsolete) {
$this->obsolete = $obsolete;
return $this;
}
public function needInlineComments($need_inline_comments) {
$this->needInlineComments = $need_inline_comments;
return $this;
}
public function setMockCache($mock_cache) {
$this->mockCache = $mock_cache;
return $this;
}
public function getMockCache() {
return $this->mockCache;
}
protected function loadPage() {
$table = new PholioImage();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$images = $table->loadAllFromArray($data);
return $images;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->mockIDs) {
$where[] = qsprintf(
$conn_r,
'mockID IN (%Ld)',
$this->mockIDs);
}
if ($this->obsolete !== null) {
$where[] = qsprintf(
$conn_r,
'isObsolete = %d',
$this->obsolete);
}
return $this->formatWhereClause($where);
}
protected function willFilterPage(array $images) {
assert_instances_of($images, 'PholioImage');
+ if ($this->getMockCache()) {
+ $mocks = $this->getMockCache();
+ } else {
+ $mock_ids = mpull($images, 'getMockID');
+ // DO NOT set needImages to true; recursion results!
+ $mocks = id(new PholioMockQuery())
+ ->setViewer($this->getViewer())
+ ->withIDs($mock_ids)
+ ->execute();
+ $mocks = mpull($mocks, null, 'getID');
+ }
+ foreach ($images as $index => $image) {
+ $mock = idx($mocks, $image->getMockID());
+ if ($mock) {
+ $image->attachMock($mock);
+ } else {
+ // mock is missing or we can't see it
+ unset($images[$index]);
+ }
+ }
+
+ return $images;
+ }
+
+ protected function didFilterPage(array $images) {
+ assert_instances_of($images, 'PholioImage');
+
$file_phids = mpull($images, 'getFilePHID');
$all_files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$all_files = mpull($all_files, null, 'getPHID');
if ($this->needInlineComments) {
$all_inline_comments = id(new PholioTransactionComment())
->loadAllWhere('imageid IN (%Ld)',
mpull($images, 'getID'));
$all_inline_comments = mgroup($all_inline_comments, 'getImageID');
}
foreach ($images as $image) {
$file = idx($all_files, $image->getFilePHID());
if (!$file) {
$file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png');
}
$image->attachFile($file);
if ($this->needInlineComments) {
$inlines = idx($all_inline_comments, $image->getID(), array());
$image->attachInlineComments($inlines);
}
}
- if ($this->getMockCache()) {
- $mocks = $this->getMockCache();
- } else {
- $mock_ids = mpull($images, 'getMockID');
- // DO NOT set needImages to true; recursion results!
- $mocks = id(new PholioMockQuery())
- ->setViewer($this->getViewer())
- ->withIDs($mock_ids)
- ->execute();
- $mocks = mpull($mocks, null, 'getID');
- }
- foreach ($images as $index => $image) {
- $mock = idx($mocks, $image->getMockID());
- if ($mock) {
- $image->attachMock($mock);
- } else {
- // mock is missing or we can't see it
- unset($images[$index]);
- }
- }
-
return $images;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index d7c9b2e073..6174b6e795 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,261 +1,265 @@
<?php
final class PhabricatorProjectProfileController
extends PhabricatorProjectController {
private $id;
private $page;
+ public function shouldAllowPublic() {
+ return true;
+ }
+
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->page = idx($data, 'page');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->needMembers(true)
->needProfiles(true)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$profile = $project->getProfile();
$picture = $profile->getProfileImageURI();
require_celerity_resource('phabricator-profile-css');
$tasks = $this->renderTasksPage($project, $profile);
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$project->getPHID(),
));
$query->setLimit(50);
$query->setViewer($this->getRequest()->getUser());
$stories = $query->execute();
$feed = $this->renderStories($stories);
$people = $this->renderPeoplePage($project, $profile);
$content = id(new AphrontMultiColumnView())
->addColumn($people)
->addColumn($feed)
->setFluidLayout(true);
$content = hsprintf(
'<div class="phabricator-project-layout">%s%s</div>',
$tasks,
$content);
$header = id(new PHUIHeaderView())
->setHeader($project->getName())
->setSubheader(phutil_utf8_shorten($profile->getBlurb(), 1024))
->setImage($picture);
$actions = $this->buildActionListView($project);
$properties = $this->buildPropertyListView($project, $actions);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($project->getName()));
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$content,
),
array(
'title' => $project->getName(),
'device' => true,
));
}
private function renderPeoplePage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$member_phids = $project->getMemberPHIDs();
$handles = $this->loadViewerHandles($member_phids);
$affiliated = array();
foreach ($handles as $phids => $handle) {
$affiliated[] = phutil_tag('li', array(), $handle->renderLink());
}
if ($affiliated) {
$affiliated = phutil_tag('ul', array(), $affiliated);
} else {
$affiliated = hsprintf('<p><em>%s</em></p>', pht(
'No one is affiliated with this project.'));
}
return hsprintf(
'<div class="phabricator-profile-info-group profile-wrap-responsive">'.
'<h1 class="phabricator-profile-info-header">%s</h1>'.
'<div class="phabricator-profile-info-pane">%s</div>'.
'</div>',
pht('People'),
$affiliated);
}
private function renderFeedPage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(array($project->getPHID()));
$query->setViewer($this->getRequest()->getUser());
$query->setLimit(100);
$stories = $query->execute();
if (!$stories) {
return pht('There are no stories about this project.');
}
return $this->renderStories($stories);
}
private function renderStories(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($this->getRequest()->getUser());
$builder->setShowHovercards(true);
$view = $builder->buildView();
return hsprintf(
'<div class="profile-feed profile-wrap-responsive">'.
'%s'.
'</div>',
$view->render());
}
private function renderTasksPage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$user = $this->getRequest()->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($user)
->withAnyProjects(array($project->getPHID()))
->withStatus(ManiphestTaskQuery::STATUS_OPEN)
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->setLimit(10);
$tasks = $query->execute();
$phids = mpull($tasks, 'getOwnerPHID');
$phids = array_merge(
$phids,
array_mergev(mpull($tasks, 'getProjectPHIDs')));
$phids = array_filter($phids);
$handles = $this->loadViewerHandles($phids);
$task_list = new ManiphestTaskListView();
$task_list->setUser($user);
$task_list->setTasks($tasks);
$task_list->setHandles($handles);
$list = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_LARGE)
->appendChild($task_list);
$content = id(new PHUIObjectBoxView())
->setHeaderText(pht('Open Tasks'))
->appendChild($list);
return $content;
}
private function buildActionListView(PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $project->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($project)
->setObjectURI($request->getRequestURI());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Project'))
->setIcon('edit')
->setHref($this->getApplicationURI("edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Members'))
->setIcon('edit')
->setHref($this->getApplicationURI("members/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action = null;
if (!$project->isUserMember($viewer->getPHID())) {
$can_join = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_JOIN);
$action = id(new PhabricatorActionView())
->setUser($viewer)
->setRenderAsForm(true)
->setHref('/project/update/'.$project->getID().'/join/')
->setIcon('new')
->setDisabled(!$can_join)
->setName(pht('Join Project'));
} else {
$action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/project/update/'.$project->getID().'/leave/')
->setIcon('delete')
->setName(pht('Leave Project...'));
}
$view->addAction($action);
return $view;
}
private function buildPropertyListView(
PhabricatorProject $project,
PhabricatorActionListView $actions) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($project)
->setActionList($actions);
$view->addProperty(
pht('Created'),
phabricator_datetime($project->getDateCreated(), $viewer));
return $view;
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index 00f6ead387..e379a48377 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,258 +1,263 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $slugs;
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 $needMembers;
private $needProfiles;
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 withPhrictionSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needProfiles($need_profiles) {
$this->needProfiles = $need_profiles;
return $this;
}
protected function getPagingColumn() {
return 'name';
}
protected function getPagingValue($result) {
return $result->getName();
}
protected function getReversePaging() {
return true;
}
protected function loadPage() {
$table = new PhabricatorProject();
$conn_r = $table->establishConnection('r');
// 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.
$select_clause = '';
if (!$this->needMembers) {
$select_clause = ', vm.dst viewerIsMember';
}
$data = queryfx_all(
$conn_r,
'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q',
$select_clause,
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$projects = $table->loadAllFromArray($data);
if ($projects) {
$viewer_phid = $this->getViewer()->getPHID();
if ($this->needMembers) {
$etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
$members = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($projects, 'getPHID'))
->withEdgeTypes(array($etype))
->execute();
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachMemberPHIDs(array_keys($members[$phid][$etype]));
$project->setIsUserMember(
$viewer_phid,
isset($members[$phid][$etype][$viewer_phid]));
}
} else {
foreach ($data as $row) {
$projects[$row['id']]->setIsUserMember(
$viewer_phid,
($row['viewerIsMember'] !== null));
}
}
+ }
- if ($this->needProfiles) {
- $profiles = id(new PhabricatorProjectProfile())->loadAllWhere(
- 'projectPHID IN (%Ls)',
- mpull($projects, 'getPHID'));
- $profiles = mpull($profiles, null, 'getProjectPHID');
-
- $default = null;
-
- if ($profiles) {
- $file_phids = mpull($profiles, 'getProfileImagePHID');
- $files = id(new PhabricatorFileQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs($file_phids)
- ->execute();
- $files = mpull($files, null, 'getPHID');
- foreach ($profiles as $profile) {
- $file = idx($files, $profile->getProfileImagePHID());
- if (!$file) {
- if (!$default) {
- $default = PhabricatorFile::loadBuiltin(
- $this->getViewer(),
- 'profile.png');
- }
- $file = $default;
- }
- $profile->attachProfileImageFile($file);
- }
- }
+ return $projects;
+ }
- foreach ($projects as $project) {
- $profile = idx($profiles, $project->getPHID());
- if (!$profile) {
+ protected function didFilterPage(array $projects) {
+ if ($this->needProfiles) {
+ $profiles = id(new PhabricatorProjectProfile())->loadAllWhere(
+ 'projectPHID IN (%Ls)',
+ mpull($projects, 'getPHID'));
+ $profiles = mpull($profiles, null, 'getProjectPHID');
+
+ $default = null;
+
+ if ($profiles) {
+ $file_phids = mpull($profiles, 'getProfileImagePHID');
+ $files = id(new PhabricatorFileQuery())
+ ->setParentQuery($this)
+ ->setViewer($this->getViewer())
+ ->withPHIDs($file_phids)
+ ->execute();
+ $files = mpull($files, null, 'getPHID');
+ foreach ($profiles as $profile) {
+ $file = idx($files, $profile->getProfileImagePHID());
+ if (!$file) {
if (!$default) {
$default = PhabricatorFile::loadBuiltin(
$this->getViewer(),
'profile.png');
}
- $profile = id(new PhabricatorProjectProfile())
- ->setProjectPHID($project->getPHID())
- ->attachProfileImageFile($default);
+ $file = $default;
+ }
+ $profile->attachProfileImageFile($file);
+ }
+ }
+
+ foreach ($projects as $project) {
+ $profile = idx($profiles, $project->getPHID());
+ if (!$profile) {
+ if (!$default) {
+ $default = PhabricatorFile::loadBuiltin(
+ $this->getViewer(),
+ 'profile.png');
}
- $project->attachProfile($profile);
+ $profile = id(new PhabricatorProjectProfile())
+ ->setProjectPHID($project->getPHID())
+ ->attachProfileImageFile($default);
}
+ $project->attachProfile($profile);
}
}
return $projects;
}
private function buildWhereClause($conn_r) {
$where = array();
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(
"Unknown project status '{$this->status}'!");
}
$where[] = qsprintf(
$conn_r,
'status IN (%Ld)',
$filter);
}
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs) {
$where[] = qsprintf(
$conn_r,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->slugs) {
$where[] = qsprintf(
$conn_r,
'phrictionSlug IN (%Ls)',
$this->slugs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function buildGroupClause($conn_r) {
if ($this->memberPHIDs) {
return 'GROUP BY p.id';
} else {
return '';
}
}
private function buildJoinClause($conn_r) {
$joins = array();
if (!$this->needMembers) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER,
$this->getViewer()->getPHID());
}
if ($this->memberPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER);
}
return implode(' ', $joins);
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index f985d43e67..ad0d7b2a33 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,413 +1,550 @@
<?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 $raisePolicyExceptions;
private $parentQuery;
private $rawResultLimit;
private $capabilities;
+ private $workspace = array();
/* -( 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("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 Exception("Call setViewer() before execute()!");
}
$parent_query = $this->getParentQuery();
if ($parent_query) {
$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;
}
try {
$page = $this->loadPage();
} catch (PhabricatorEmptyQueryException $ex) {
$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);
}
+ 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);
if (!$this->capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
} else {
$capabilities = $this->capabilities;
}
$filter->requireCapabilities($capabilities);
$filter->raisePolicyExceptions($this->shouldRaisePolicyExceptions());
return $filter;
}
protected function didRejectResult(PhabricatorPolicyInterface $object) {
$this->getPolicyFilter()->rejectObject(
$object,
$object->getPolicy(PhabricatorPolicyCapability::CAN_VIEW),
PhabricatorPolicyCapability::CAN_VIEW);
}
+/* -( 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.
+ *
+ * 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) {
+ assert_instances_of($objects, 'PhabricatorPolicyInterface');
+
+ $viewer_phid = $this->getViewer()->getPHID();
+
+ // The workspace is scoped per viewer to prevent accidental contamination.
+ if (empty($this->workspace[$viewer_phid])) {
+ $this->workspace[$viewer_phid] = array();
+ }
+
+ $this->workspace[$viewer_phid] += $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 retreive.
+ * @return this
+ * @task workspace
+ */
+ public function getObjectsFromWorkspace(array $phids) {
+ $viewer_phid = $this->getViewer()->getPHID();
+
+ $results = array();
+ foreach ($phids as $key => $phid) {
+ if (isset($this->workspace[$viewer_phid][$phid])) {
+ $results[$phid] = $this->workspace[$viewer_phid][$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;
+ }
+
+
/* -( 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.
+ * 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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:06 (3 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1125787
Default Alt Text
(94 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment