1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-15 18:10:53 +01:00

(stable) Promote 2017 Week 14

This commit is contained in:
epriestley 2017-04-07 13:40:26 -07:00
commit 699ab153e3
89 changed files with 1389 additions and 582 deletions

View file

@ -0,0 +1,8 @@
CREATE TABLE {$NAMESPACE}_auth.auth_hmackey (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
keyName VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
keyValue VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_name` (keyName)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -14,7 +14,7 @@ foreach ($sessions as $session) {
$conn,
'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s',
PhabricatorUser::SESSION_TABLE,
PhabricatorHash::digest($session['sessionKey']),
PhabricatorHash::weakDigest($session['sessionKey']),
$session['userPHID'],
$session['type']);
}

View file

@ -502,6 +502,7 @@ phutil_register_library_map(array(
'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php',
'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php',
'DifferentialReviewersSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialReviewersSearchEngineAttachment.php',
'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
'DifferentialRevisionAbandonTransaction' => 'applications/differential/xaction/DifferentialRevisionAbandonTransaction.php',
@ -1919,6 +1920,7 @@ phutil_register_library_map(array(
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
@ -2743,7 +2745,6 @@ phutil_register_library_map(array(
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
'PhabricatorFileCommentController' => 'applications/files/controller/PhabricatorFileCommentController.php',
'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
@ -2751,6 +2752,7 @@ phutil_register_library_map(array(
'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php',
'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php',
'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php',
@ -2762,13 +2764,16 @@ phutil_register_library_map(array(
'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php',
'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php',
'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php',
'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php',
'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php',
'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php',
'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php',
'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
'PhabricatorFileSearchConduitAPIMethod' => 'applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php',
'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
@ -2783,6 +2788,7 @@ phutil_register_library_map(array(
'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
'PhabricatorFileTransactionType' => 'applications/files/xaction/PhabricatorFileTransactionType.php',
'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
@ -2805,6 +2811,7 @@ phutil_register_library_map(array(
'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php',
'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php',
'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php',
'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php',
'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
@ -2858,6 +2865,7 @@ phutil_register_library_map(array(
'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php',
'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php',
'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php',
'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php',
'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php',
'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
@ -5271,6 +5279,7 @@ phutil_register_library_map(array(
'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewersField' => 'DifferentialCoreCustomField',
'DifferentialReviewersHeraldAction' => 'HeraldAction',
'DifferentialReviewersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialReviewersView' => 'AphrontView',
'DifferentialRevision' => array(
'DifferentialDAO',
@ -6893,6 +6902,7 @@ phutil_register_library_map(array(
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
'PhabricatorAuthHighSecurityToken' => 'Phobject',
'PhabricatorAuthInvite' => array(
@ -7845,6 +7855,7 @@ phutil_register_library_map(array(
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileBundleLoader' => 'Phobject',
@ -7858,7 +7869,6 @@ phutil_register_library_map(array(
'Iterator',
),
'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileCommentController' => 'PhabricatorFileController',
'PhabricatorFileComposeController' => 'PhabricatorFileController',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
@ -7866,6 +7876,7 @@ phutil_register_library_map(array(
'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileEditController' => 'PhabricatorFileController',
'PhabricatorFileEditEngine' => 'PhabricatorEditEngine',
'PhabricatorFileEditField' => 'PhabricatorEditField',
'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorFileExternalRequest' => array(
@ -7887,13 +7898,16 @@ phutil_register_library_map(array(
'PhabricatorFileImageProxyController' => 'PhabricatorFileController',
'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
'PhabricatorFileInfoController' => 'PhabricatorFileController',
'PhabricatorFileIntegrityException' => 'Exception',
'PhabricatorFileLightboxController' => 'PhabricatorFileController',
'PhabricatorFileLinkView' => 'AphrontTagView',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileStorageConfigurationException' => 'Exception',
@ -7905,9 +7919,10 @@ phutil_register_library_map(array(
'PhabricatorFileTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
'PhabricatorFileTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorFileTransaction' => 'PhabricatorModularTransaction',
'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorFileTransform' => 'Phobject',
'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileTransformListController' => 'PhabricatorFileController',
@ -7930,6 +7945,7 @@ phutil_register_library_map(array(
'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
@ -7985,6 +8001,7 @@ phutil_register_library_map(array(
'PhabricatorGuideModule' => 'Phobject',
'PhabricatorGuideModuleController' => 'PhabricatorGuideController',
'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule',
'PhabricatorHMACTestCase' => 'PhabricatorTestCase',
'PhabricatorHTTPParameterTypeTableView' => 'AphrontView',
'PhabricatorHandleList' => array(
'Phobject',

View file

@ -94,10 +94,15 @@ final class AphrontFileResponse extends AphrontResponse {
array('Accept-Ranges', 'bytes'),
);
if ($this->rangeMin || $this->rangeMax) {
if ($this->rangeMin !== null || $this->rangeMax !== null) {
$len = $this->getContentLength();
$min = $this->rangeMin;
$max = $this->rangeMax;
if ($max === null) {
$max = ($len - 1);
}
$headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}");
$content_len = ($max - $min) + 1;
} else {
@ -105,7 +110,7 @@ final class AphrontFileResponse extends AphrontResponse {
}
if (!$this->shouldCompressResponse()) {
$headers[] = array('Content-Length', $this->getContentLength());
$headers[] = array('Content-Length', $content_len);
}
if (strlen($this->getDownload())) {

View file

@ -194,7 +194,7 @@ abstract class PhabricatorAuthController extends PhabricatorController {
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::digest($registration_key);
$expect = PhabricatorHash::weakDigest($registration_key);
if (!phutil_hashes_are_identical($actual, $expect)) {
$response = $this->renderError(
pht(

View file

@ -194,7 +194,7 @@ final class PhabricatorAuthLoginController
$registration_key = Filesystem::readRandomCharacters(32);
$account->setProperty(
'registrationKey',
PhabricatorHash::digest($registration_key));
PhabricatorHash::weakDigest($registration_key));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();

View file

@ -135,7 +135,7 @@ final class PhabricatorAuthOneTimeLoginController
->setTokenResource($target_user->getPHID())
->setTokenType($password_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->setTokenCode(PhabricatorHash::weakDigest($key))
->save();
unset($unguarded);

View file

@ -24,11 +24,11 @@ final class PhabricatorAuthSSHKeyGenerateController
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$file = PhabricatorFile::buildFromFileDataOrHash(
$file = PhabricatorFile::newFromFileData(
$private_key,
array(
'name' => $default_name.'.key',
'ttl' => time() + (60 * 10),
'ttl.relative' => phutil_units('10 minutes in seconds'),
'viewPolicy' => $viewer->getPHID(),
));

View file

@ -16,7 +16,7 @@ final class PhabricatorAuthTerminateSessionController
$query->withIDs(array($id));
}
$current_key = PhabricatorHash::digest(
$current_key = PhabricatorHash::weakDigest(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
$sessions = $query->execute();

View file

@ -110,7 +110,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
$conn_r = $session_table->establishConnection('r');
$session_key = PhabricatorHash::digest($session_token);
$session_key = PhabricatorHash::weakDigest($session_token);
$cache_parts = $this->getUserCacheQueryParts($conn_r);
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
@ -240,7 +240,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
$digest_key = PhabricatorHash::digest($session_key);
$digest_key = PhabricatorHash::weakDigest($session_key);
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
@ -306,7 +306,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
->execute();
if ($except_session !== null) {
$except_session = PhabricatorHash::digest($except_session);
$except_session = PhabricatorHash::weakDigest($except_session);
}
foreach ($sessions as $key => $session) {
@ -755,7 +755,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$parts[] = $email->getVerificationCode();
}
return PhabricatorHash::digest(implode(':', $parts));
return PhabricatorHash::weakDigest(implode(':', $parts));
}

View file

@ -39,7 +39,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($totp_token_type))
->withExpired(false)
->withTokenCodes(array(PhabricatorHash::digest($key)))
->withTokenCodes(array(PhabricatorHash::weakDigest($key)))
->executeOne();
if (!$temporary_token) {
// If we don't have a matching token, regenerate the key below.
@ -58,7 +58,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
->setTokenResource($user->getPHID())
->setTokenType($totp_token_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->setTokenCode(PhabricatorHash::weakDigest($key))
->save();
unset($unguarded);
}

View file

@ -474,7 +474,7 @@ abstract class PhabricatorAuthProvider extends Phobject {
true);
}
return PhabricatorHash::digest($phcid);
return PhabricatorHash::weakDigest($phcid);
}
protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {

View file

@ -85,7 +85,7 @@ final class PhabricatorAuthSessionQuery
if ($this->sessionKeys) {
$hashes = array();
foreach ($this->sessionKeys as $session_key) {
$hashes[] = PhabricatorHash::digest($session_key);
$hashes[] = PhabricatorHash::weakDigest($session_key);
}
$where[] = qsprintf(
$conn_r,

View file

@ -0,0 +1,24 @@
<?php
final class PhabricatorAuthHMACKey
extends PhabricatorAuthDAO {
protected $keyName;
protected $keyValue;
protected function getConfiguration() {
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'keyName' => 'text64',
'keyValue' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('keyName'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
}

View file

@ -43,7 +43,7 @@ final class PhabricatorBadgesBadgeNameTransaction
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newRequiredError(
$errors[] = $this->newInvalidError(
pht('The name can be no longer than %s characters.',
new PhutilNumber($max_length)));
}

View file

@ -98,7 +98,7 @@ abstract class PhabricatorController extends AphrontController {
if (!$user->isLoggedIn()) {
$user->attachAlternateCSRFString(PhabricatorHash::digest($phsid));
$user->attachAlternateCSRFString(PhabricatorHash::weakDigest($phsid));
}
$request->setUser($user);

View file

@ -1182,9 +1182,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "calendar:T{$id}:{$field}:{$hash}";
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}

View file

@ -14,7 +14,7 @@ abstract class CelerityResources extends Phobject {
public function getCelerityHash($data) {
$tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
$hash = PhabricatorHash::digest($data, $tail);
$hash = PhabricatorHash::weakDigest($data, $tail);
return substr($hash, 0, 8);
}

View file

@ -27,7 +27,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication {
}
public function getName() {
return 'Config';
return pht('Config');
}
public function getShortDescription() {

View file

@ -107,7 +107,7 @@ final class PhabricatorPathSetupCheck extends PhabricatorSetupCheck {
if ($bad_paths) {
foreach ($bad_paths as $path_part => $message) {
$digest = substr(PhabricatorHash::digest($path_part), 0, 8);
$digest = substr(PhabricatorHash::weakDigest($path_part), 0, 8);
$this
->newIssue('config.PATH.'.$digest)

View file

@ -109,7 +109,8 @@ EOTEXT
'Default key for HMAC digests where the key is not important '.
'(i.e., the hash itself is secret). You can change this if you '.
'want (to any other string), but doing so will break existing '.
'sessions and CSRF tokens.')),
'sessions and CSRF tokens. This option is deprecated. Newer '.
'code automatically manages HMAC keys.')),
$this->newOption('security.require-https', 'bool', false)
->setLocked(true)
->setSummary(

View file

@ -357,7 +357,7 @@ final class DifferentialChangesetViewController extends DifferentialController {
array(
'name' => $changeset->getFilename(),
'mime-type' => 'text/plain',
'ttl' => phutil_units('24 hours in seconds'),
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));

View file

@ -889,15 +889,15 @@ final class DifferentialRevisionViewController extends DifferentialController {
}
$file_name .= 'diff';
$file = PhabricatorFile::buildFromFileDataOrHash(
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$raw_diff,
array(
'name' => $file_name,
'ttl' => (60 * 60 * 24),
'ttl.relative' => phutil_units('24 hours in seconds'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject($revision->getPHID());
unset($unguarded);

View file

@ -0,0 +1,41 @@
<?php
final class DifferentialReviewersSearchEngineAttachment
extends PhabricatorSearchEngineAttachment {
public function getAttachmentName() {
return pht('Differential Reviewers');
}
public function getAttachmentDescription() {
return pht('Get the reviewers for each revision.');
}
public function willLoadAttachmentData($query, $spec) {
$query->needReviewers(true);
}
public function getAttachmentForObject($object, $data, $spec) {
$reviewers = $object->getReviewers();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$list = array();
foreach ($reviewers as $reviewer) {
$status = $reviewer->getReviewerStatus();
$is_blocking = ($status == $status_blocking);
$list[] = array(
'reviewerPHID' => $reviewer->getReviewerPHID(),
'status' => $status,
'isBlocking' => $is_blocking,
'actorPHID' => $reviewer->getLastActorPHID(),
);
}
return array(
'reviewers' => $list,
);
}
}

View file

@ -260,8 +260,8 @@ final class DifferentialInlineComment
public function getMarkupFieldKey($field) {
// We can't use ID because synthetic comments don't have it.
return 'DI:'.PhabricatorHash::digest($this->getContent());
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {

View file

@ -907,7 +907,10 @@ final class DifferentialRevision extends DifferentialDAO
}
public function getConduitSearchAttachments() {
return array();
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}

View file

@ -183,8 +183,19 @@ abstract class DifferentialRevisionReviewTransaction
// In all cases, you affect yourself.
$map[$viewer->getPHID()] = $status;
// If the user has submitted a specific list of reviewers to act as (by
// unchecking some checkboxes under "Accept"), only affect those reviewers.
// If we're applying an "accept the defaults" transaction, and this
// transaction type uses checkboxes, replace the value with the list of
// defaults.
if (!is_array($value)) {
list($options, $default) = $this->getActionOptions($viewer, $revision);
if ($options) {
$value = $default;
}
}
// If we have a specific list of reviewers to act on, usually because the
// user has submitted a specific list of reviewers to act as by
// unchecking some checkboxes under "Accept", only affect those reviewers.
if (is_array($value)) {
$map = array_select_keys($map, $value);
}

View file

@ -14,7 +14,7 @@ final class DiffusionRepositoryListController extends DiffusionController {
->setName(pht('Commits'));
$items[] = id(new PHUIListItemView())
->setName('Browse Commits')
->setName(pht('Browse Commits'))
->setHref($this->getApplicationURI('commit/'));
return id(new PhabricatorRepositorySearchEngine())

View file

@ -652,7 +652,7 @@ final class DiffusionServeController extends DiffusionController {
}
$lfs_pass = $password->openEnvelope();
$lfs_hash = PhabricatorHash::digest($lfs_pass);
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())

View file

@ -507,6 +507,10 @@ final class DiffusionURIEditor
->synchronizeWorkingCopyAfterHostingChange();
}
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
null);
return $xactions;
}

View file

@ -22,7 +22,7 @@ final class DiffusionGitLFSTemporaryTokenType
$lfs_user = self::HTTP_USERNAME;
$lfs_pass = Filesystem::readRandomCharacters(32);
$lfs_hash = PhabricatorHash::digest($lfs_pass);
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
$ttl = PhabricatorTime::getNow() + phutil_units('1 day in seconds');

View file

@ -93,7 +93,7 @@ abstract class DiffusionFileFutureQuery
$drequest = $this->getRequest();
$name = basename($drequest->getPath());
$ttl = PhabricatorTime::getNow() + phutil_units('48 hours in seconds');
$relative_ttl = phutil_units('48 hours in seconds');
try {
$threshold = PhabricatorFileStorageEngine::getChunkThreshold();
@ -101,7 +101,7 @@ abstract class DiffusionFileFutureQuery
$source = id(new PhabricatorExecFutureFileUploadSource())
->setName($name)
->setTTL($ttl)
->setRelativeTTL($relative_ttl)
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
->setExecFuture($future);

View file

@ -22,7 +22,7 @@ final class DrydockManagementLeaseWorkflow
array(
'name' => 'attributes',
'param' => 'name=value,...',
'help' => pht('Resource specficiation.'),
'help' => pht('Resource specification.'),
),
));
}

View file

@ -15,7 +15,7 @@ final class PhabricatorImageTransformer extends Phobject {
$image,
array(
'name' => 'meme-'.$file->getName(),
'ttl' => time() + 60 * 60 * 24,
'ttl.relative' => phutil_units('24 hours in seconds'),
'canCDN' => true,
));
}

View file

@ -78,7 +78,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
'comment/(?P<id>[1-9]\d*)/' => 'PhabricatorFileCommentController',
'thread/(?P<phid>[^/]+)/' => 'PhabricatorFileLightboxController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
$this->getEditRoutePattern('edit/')
=> 'PhabricatorFileEditController',
'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'imageproxy/' => 'PhabricatorFileImageProxyController',
'transforms/(?P<id>[1-9]\d*)/' =>

View file

@ -36,23 +36,26 @@ final class FileAllocateConduitAPIMethod
$properties = array(
'name' => $name,
'authorPHID' => $viewer->getPHID(),
'viewPolicy' => $view_policy,
'isExplicitUpload' => true,
);
if ($view_policy !== null) {
$properties['viewPolicy'] = $view_policy;
}
$ttl = $request->getValue('deleteAfterEpoch');
if ($ttl) {
$properties['ttl'] = $ttl;
$properties['ttl.absolute'] = $ttl;
}
$file = null;
if ($hash) {
if ($hash !== null) {
$file = PhabricatorFile::newFileFromContentHash(
$hash,
$properties);
}
if ($hash && !$file) {
if ($hash !== null && !$file) {
$chunked_hash = PhabricatorChunkedFileStorageEngine::getChunkedHash(
$viewer,
$hash);
@ -103,7 +106,7 @@ final class FileAllocateConduitAPIMethod
if ($chunk_engines) {
$chunk_properties = $properties;
if ($hash) {
if ($hash !== null) {
$chunk_properties += array(
'chunkedHash' => $chunked_hash,
);

View file

@ -10,6 +10,16 @@ final class FileInfoConduitAPIMethod extends FileConduitAPIMethod {
return pht('Get information about a file.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_FROZEN;
}
public function getMethodStatusDescription() {
return pht(
'This method is frozen and will eventually be deprecated. New code '.
'should use "file.search" instead.');
}
protected function defineParamTypes() {
return array(
'phid' => 'optional phid',

View file

@ -27,21 +27,27 @@ final class FileUploadConduitAPIMethod extends FileConduitAPIMethod {
$viewer = $request->getUser();
$name = $request->getValue('name');
$can_cdn = $request->getValue('canCDN');
$can_cdn = (bool)$request->getValue('canCDN');
$view_policy = $request->getValue('viewPolicy');
$data = $request->getValue('data_base64');
$data = $this->decodeBase64($data);
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $name,
$params = array(
'authorPHID' => $viewer->getPHID(),
'viewPolicy' => $view_policy,
'canCDN' => $can_cdn,
'isExplicitUpload' => true,
));
);
if ($name !== null) {
$params['name'] = $name;
}
if ($view_policy !== null) {
$params['viewPolicy'] = $view_policy;
}
$file = PhabricatorFile::newFromFileData($data, $params);
return $file->getPHID();
}

View file

@ -3,12 +3,21 @@
final class FileUploadHashConduitAPIMethod extends FileConduitAPIMethod {
public function getAPIMethodName() {
// TODO: Deprecate this in favor of `file.allocate`.
return 'file.uploadhash';
}
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return pht(
'This method is deprecated. Callers should use "file.allocate" '.
'instead.');
}
public function getMethodDescription() {
return pht('Upload a file to the server using content hash.');
return pht('Obsolete. Has no effect.');
}
protected function defineParamTypes() {
@ -19,25 +28,11 @@ final class FileUploadHashConduitAPIMethod extends FileConduitAPIMethod {
}
protected function defineReturnType() {
return 'phid or null';
return 'null';
}
protected function execute(ConduitAPIRequest $request) {
$hash = $request->getValue('hash');
$name = $request->getValue('name');
$user = $request->getUser();
$file = PhabricatorFile::newFileFromContentHash(
$hash,
array(
'name' => $name,
'authorPHID' => $user->getPHID(),
));
if ($file) {
return $file->getPHID();
}
return $file;
return null;
}
}

View file

@ -0,0 +1,18 @@
<?php
final class PhabricatorFileSearchConduitAPIMethod
extends PhabricatorSearchEngineAPIMethod {
public function getAPIMethodName() {
return 'file.search';
}
public function newSearchEngine() {
return new PhabricatorFileSearchEngine();
}
public function getMethodSummary() {
return pht('Read information about files.');
}
}

View file

@ -1,62 +0,0 @@
<?php
final class PhabricatorFileCommentController extends PhabricatorFileController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$draft = PhabricatorDraft::buildFromRequest($request);
$view_uri = $file->getInfoURI();
$xactions = array();
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorFileTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new PhabricatorFileEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($file, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($draft) {
$draft->replaceOrDelete();
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}

View file

@ -64,14 +64,21 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$range = $request->getHTTPHeader('range');
if ($range) {
$matches = null;
if (preg_match('/^bytes=(\d+)-(\d+)$/', $range, $matches)) {
if (preg_match('/^bytes=(\d+)-(\d*)$/', $range, $matches)) {
// Note that the "Range" header specifies bytes differently than
// we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1).
$begin = (int)$matches[1];
$end = (int)$matches[2] + 1;
// The "Range" may be "200-299" or "200-", meaning "until end of file".
if (strlen($matches[2])) {
$range_end = (int)$matches[2];
$end = $range_end + 1;
} else {
$range_end = null;
}
$response->setHTTPResponseCode(206);
$response->setRange($begin, ($end - 1));
$response->setRange($begin, $range_end);
}
}
@ -84,18 +91,28 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
if ($is_viewable && !$force_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
if (!$request->isHTTPPost() && !$is_alternate_domain && !$is_lfs) {
// NOTE: Require POST to download files from the primary domain. We'd
// rather go full-bore and do a real CSRF check, but can't currently
// authenticate users on the file domain. This should blunt any
// attacks based on iframes, script tags, applet tags, etc., at least.
// Send the user to the "info" page if they're using some other method.
$is_public = !$viewer->isLoggedIn();
$is_post = $request->isHTTPPost();
// NOTE: Require POST to download files from the primary domain if the
// request includes credentials. The "Download File" links we generate
// in the web UI are forms which use POST to satisfy this requirement.
// The intent is to make attacks based on tags like "<iframe />" and
// "<script />" (which can issue GET requests, but can not easily issue
// POST requests) more difficult to execute.
// The best defense against these attacks is to use an alternate file
// domain, which is why we strongly recommend doing so.
$is_safe = ($is_alternate_domain || $is_lfs || $is_post || $is_public);
if (!$is_safe) {
// This is marked as "external" because it is fully qualified.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
}
$response->setMimeType($file->getMimeType());
$response->setDownload($file->getName());
}

View file

@ -1,114 +1,12 @@
<?php
final class PhabricatorFileEditController extends PhabricatorFileController {
final class PhabricatorFileEditController
extends PhabricatorFileController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$title = pht('Edit File: %s', $file->getName());
$file_name = $file->getName();
$header_icon = 'fa-pencil';
$view_uri = '/'.$file->getMonogram();
$error_name = true;
$validation_exception = null;
if ($request->isFormPost()) {
$can_view = $request->getStr('canView');
$file_name = $request->getStr('name');
$errors = array();
$type_name = PhabricatorFileTransaction::TYPE_NAME;
$xactions = array();
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($can_view);
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorFileTransaction::TYPE_NAME)
->setNewValue($file_name);
$editor = id(new PhabricatorFileEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($file, $xactions);
return id(new AphrontRedirectResponse())->setURI($view_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$error_name = $ex->getShortMessage($type_name);
$file->setViewPolicy($can_view);
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($file)
->execute();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setValue($file_name)
->setLabel(pht('Name'))
->setError($error_name))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($file)
->setPolicies($policies)
->setName('canView'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($view_uri)
->setValue(pht('Save Changes')));
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($file->getMonogram(), $view_uri)
->addTextCrumb(pht('Edit'))
->setBorder(true);
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($form);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon($header_icon);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
return id(new PhabricatorFileEditEngine())
->setController($this)
->buildResponse();
}
}

View file

@ -94,25 +94,18 @@ final class PhabricatorFileInfoController extends PhabricatorFileController {
$file,
new PhabricatorFileTransactionQuery());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$comment_view = id(new PhabricatorFileEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($file);
$add_comment_header = $is_serious
? pht('Add Comment')
: pht('Question File Integrity');
$monogram = $file->getMonogram();
$draft = PhabricatorDraft::newFromUserAndKey($viewer, $file->getPHID());
$add_comment_form = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($file->getPHID())
->setDraft($draft)
->setHeaderText($add_comment_header)
->setAction($this->getApplicationURI('/comment/'.$file->getID().'/'))
->setSubmitButtonName(pht('Add Comment'));
$timeline->setQuoteRef($monogram);
$comment_view->setTransactionTimeline($timeline);
return array(
$timeline,
$add_comment_form,
$comment_view,
);
}
@ -219,6 +212,18 @@ final class PhabricatorFileInfoController extends PhabricatorFileController {
pht('Mime Type'),
$file->getMimeType());
$ttl = $file->getTtl();
if ($ttl) {
$delta = $ttl - PhabricatorTime::getNow();
$finfo->addProperty(
pht('Expires'),
pht(
'%s (%s)',
phabricator_datetime($ttl, $viewer),
phutil_format_relative_time_detailed($delta)));
}
$width = $file->getImageWidth();
if ($width) {
$finfo->addProperty(

View file

@ -0,0 +1,79 @@
<?php
final class PhabricatorFileEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'files.file';
public function getEngineName() {
return pht('Files');
}
protected function supportsEditEngineConfiguration() {
return false;
}
protected function getCreateNewObjectPolicy() {
// TODO: For now, this EditEngine can only edit objects, since there is
// a lot of complexity in dealing with file data during file creation.
return PhabricatorPolicies::POLICY_NOONE;
}
public function getSummaryHeader() {
return pht('Configure Files Forms');
}
public function getSummaryText() {
return pht('Configure creation and editing forms in Files.');
}
public function getEngineApplicationClass() {
return 'PhabricatorFilesApplication';
}
protected function newEditableObject() {
return PhabricatorFile::initializeNewFile();
}
protected function newObjectQuery() {
return new PhabricatorFileQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create New File');
}
protected function getObjectEditTitleText($object) {
return pht('Edit File: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create File');
}
protected function getObjectName() {
return pht('File');
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function buildCustomEditFields($object) {
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setTransactionType(PhabricatorFileNameTransaction::TRANSACTIONTYPE)
->setDescription(pht('The name of the file.'))
->setConduitDescription(pht('Rename the file.'))
->setConduitTypeDescription(pht('New file name.'))
->setValue($object->getName()),
);
}
}

View file

@ -17,46 +17,9 @@ final class PhabricatorFileEditor
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorFileTransaction::TYPE_NAME;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorFileTransaction::TYPE_NAME:
return $object->getName();
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorFileTransaction::TYPE_NAME:
return $xaction->getNewValue();
}
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorFileTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
break;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
@ -111,34 +74,4 @@ final class PhabricatorFileEditor
return false;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorFileTransaction::TYPE_NAME:
$missing = $this->validateIsEmptyTextField(
$object->getName(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('File name is required.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
}
return $errors;
}
}

View file

@ -102,7 +102,7 @@ final class PhabricatorChunkedFileStorageEngine
}
public static function getChunkedHashForInput($input) {
$rehash = PhabricatorHash::digest($input);
$rehash = PhabricatorHash::weakDigest($input);
// Add a suffix to identify this as a chunk hash.
$rehash = substr($rehash, 0, -2).'-C';
@ -174,7 +174,16 @@ final class PhabricatorChunkedFileStorageEngine
return (4 * 1024 * 1024);
}
public function getRawFileDataIterator(PhabricatorFile $file, $begin, $end) {
public function getRawFileDataIterator(
PhabricatorFile $file,
$begin,
$end,
PhabricatorFileStorageFormat $format) {
// NOTE: It is currently impossible for files stored with the chunk
// engine to have their own formatting (instead, the individual chunks
// are formatted), so we ignore the format object.
$chunks = id(new PhabricatorFileChunkQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withChunkHandles(array($file->getStorageHandle()))

View file

@ -16,6 +16,8 @@
*/
abstract class PhabricatorFileStorageEngine extends Phobject {
const HMAC_INTEGRITY = 'file.integrity';
/**
* Construct a new storage engine.
*
@ -325,10 +327,32 @@ abstract class PhabricatorFileStorageEngine extends Phobject {
return $engine->getChunkSize();
}
public function getRawFileDataIterator(PhabricatorFile $file, $begin, $end) {
// The default implementation is trivial and just loads the entire file
// upfront.
$data = $this->readFile($file->getStorageHandle());
public function getRawFileDataIterator(
PhabricatorFile $file,
$begin,
$end,
PhabricatorFileStorageFormat $format) {
$formatted_data = $this->readFile($file->getStorageHandle());
$known_integrity = $file->getIntegrityHash();
if ($known_integrity !== null) {
$new_integrity = $this->newIntegrityHash($formatted_data, $format);
if (!phutil_hashes_are_identical($known_integrity, $new_integrity)) {
throw new PhabricatorFileIntegrityException(
pht(
'File data integrity check failed. Dark forces have corrupted '.
'or tampered with this file. The file data can not be read.'));
}
}
$formatted_data = array($formatted_data);
$data = '';
$format_iterator = $format->newReadIterator($formatted_data);
foreach ($format_iterator as $raw_chunk) {
$data .= $raw_chunk;
}
if ($begin !== null && $end !== null) {
$data = substr($data, $begin, ($end - $begin));
@ -341,4 +365,18 @@ abstract class PhabricatorFileStorageEngine extends Phobject {
return array($data);
}
public function newIntegrityHash(
$data,
PhabricatorFileStorageFormat $format) {
$hmac_name = self::HMAC_INTEGRITY;
$data_hash = PhabricatorHash::digestWithNamedKey($data, $hmac_name);
$format_hash = $format->newFormatIntegrityHash();
$full_hash = "{$data_hash}/{$format_hash}";
return PhabricatorHash::digestWithNamedKey($full_hash, $hmac_name);
}
}

View file

@ -47,4 +47,8 @@ final class PhabricatorTestStorageEngine
unset(self::$storage[$handle]);
}
public function tamperWithFile($handle, $data) {
self::$storage[$handle] = $data;
}
}

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorFileIntegrityException
extends Exception {}

View file

@ -56,6 +56,22 @@ final class PhabricatorFileAES256StorageFormat
return array($data);
}
public function newFormatIntegrityHash() {
$file = $this->getFile();
list($key_envelope, $iv_envelope) = $this->extractKeyAndIV($file);
// NOTE: We include the IV in the format integrity hash. If we do not,
// attackers can potentially forge the first block of decrypted data
// in CBC mode if they are able to substitute a chosen IV and predict
// the plaintext. (Normally, they can not tamper with the IV.)
$input = self::FORMATKEY.'/iv:'.$iv_envelope->openEnvelope();
return PhabricatorHash::digestWithNamedKey(
$input,
PhabricatorFileStorageEngine::HMAC_INTEGRITY);
}
public function newStorageProperties() {
// Generate a unique key and IV for this block of data.
$key_envelope = self::newAES256Key();

View file

@ -22,6 +22,10 @@ abstract class PhabricatorFileStorageFormat
abstract public function newReadIterator($raw_iterator);
abstract public function newWriteIterator($raw_iterator);
public function newFormatIntegrityHash() {
return null;
}
public function newStorageProperties() {
return array();
}

View file

@ -33,6 +33,15 @@ final class PhabricatorFileStorageFormatTestCase extends PhabricatorTestCase {
// The actual raw data in the storage engine should be encoded.
$raw_data = $engine->readFile($file->getStorageHandle());
$this->assertEqual($expect, $raw_data);
// If we generate an iterator over a slice of the file, it should return
// the decrypted file.
$iterator = $file->getFileDataIterator(4, 14);
$raw_data = '';
foreach ($iterator as $data_chunk) {
$raw_data .= $data_chunk;
}
$this->assertEqual('cow jumped', $raw_data);
}
public function testAES256Storage() {
@ -73,5 +82,55 @@ final class PhabricatorFileStorageFormatTestCase extends PhabricatorTestCase {
// input data.
$raw_data = $engine->readFile($file->getStorageHandle());
$this->assertTrue($data !== $raw_data);
// If we generate an iterator over a slice of the file, it should return
// the decrypted file.
$iterator = $file->getFileDataIterator(4, 14);
$raw_data = '';
foreach ($iterator as $data_chunk) {
$raw_data .= $data_chunk;
}
$this->assertEqual('cow jumped', $raw_data);
$iterator = $file->getFileDataIterator(4, null);
$raw_data = '';
foreach ($iterator as $data_chunk) {
$raw_data .= $data_chunk;
}
$this->assertEqual('cow jumped over the full moon.', $raw_data);
}
public function testStorageTampering() {
$engine = new PhabricatorTestStorageEngine();
$good = 'The cow jumped over the full moon.';
$evil = 'The cow slept quietly, honoring the glorious dictator.';
$params = array(
'name' => 'message.txt',
'storageEngines' => array(
$engine,
),
);
// First, write the file normally.
$file = PhabricatorFile::newFromFileData($good, $params);
$this->assertEqual($good, $file->loadFileData());
// As an adversary, tamper with the file.
$engine->tamperWithFile($file->getStorageHandle(), $evil);
// Attempts to read the file data should now fail the integrity check.
$caught = null;
try {
$file->loadFileData();
} catch (PhabricatorFileIntegrityException $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof PhabricatorFileIntegrityException);
}
}

View file

@ -19,6 +19,13 @@ final class PhabricatorFilesManagementCatWorkflow
'param' => 'bytes',
'help' => pht('End printing at a specific offset.'),
),
array(
'name' => 'salvage',
'help' => pht(
'DANGEROUS. Attempt to salvage file content even if the '.
'integrity check fails. If an adversary has tampered with '.
'the file, the conent may be unsafe.'),
),
array(
'name' => 'names',
'wildcard' => true,
@ -43,10 +50,29 @@ final class PhabricatorFilesManagementCatWorkflow
$begin = $args->getArg('begin');
$end = $args->getArg('end');
$file->makeEphemeral();
// If we're running in "salvage" mode, wipe out any integrity hash which
// may be present. This makes us read file data without performing an
// integrity check.
$salvage = $args->getArg('salvage');
if ($salvage) {
$file->setIntegrityHash(null);
}
try {
$iterator = $file->getFileDataIterator($begin, $end);
foreach ($iterator as $data) {
echo $data;
}
} catch (PhabricatorFileIntegrityException $ex) {
throw new PhutilArgumentUsageException(
pht(
'File data integrity check failed. Use "--salvage" to bypass '.
'integrity checks. This flag is dangerous, use it at your own '.
'risk. Underlying error: %s',
$ex->getMessage()));
}
return 0;
}

View file

@ -0,0 +1,325 @@
<?php
final class PhabricatorFilesManagementIntegrityWorkflow
extends PhabricatorFilesManagementWorkflow {
protected function didConstruct() {
$this
->setName('integrity')
->setSynopsis(pht('Verify or recalculate file integrity hashes.'))
->setArguments(
array(
array(
'name' => 'all',
'help' => pht('Affect all files.'),
),
array(
'name' => 'strip',
'help' => pht(
'DANGEROUS. Strip integrity hashes from files. This makes '.
'files vulnerable to corruption or tampering.'),
),
array(
'name' => 'corrupt',
'help' => pht(
'Corrupt integrity hashes for given files. This is intended '.
'for debugging.'),
),
array(
'name' => 'compute',
'help' => pht(
'Compute and update integrity hashes for files which do not '.
'yet have them.'),
),
array(
'name' => 'overwrite',
'help' => pht(
'DANGEROUS. Recompute and update integrity hashes, overwriting '.
'invalid hashes. This may mark corrupt or dangerous files as '.
'valid.'),
),
array(
'name' => 'force',
'short' => 'f',
'help' => pht(
'Execute dangerous operations without prompting for '.
'confirmation.'),
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$modes = array();
$is_strip = $args->getArg('strip');
if ($is_strip) {
$modes[] = 'strip';
}
$is_corrupt = $args->getArg('corrupt');
if ($is_corrupt) {
$modes[] = 'corrupt';
}
$is_compute = $args->getArg('compute');
if ($is_compute) {
$modes[] = 'compute';
}
$is_overwrite = $args->getArg('overwrite');
if ($is_overwrite) {
$modes[] = 'overwrite';
}
$is_verify = !$modes;
if ($is_verify) {
$modes[] = 'verify';
}
if (count($modes) > 1) {
throw new PhutilArgumentUsageException(
pht(
'You have selected multiple operation modes (%s). Choose a '.
'single mode to operate in.',
implode(', ', $modes)));
}
$is_force = $args->getArg('force');
if (!$is_force) {
$prompt = null;
if ($is_strip) {
$prompt = pht(
'Stripping integrity hashes is dangerous and makes files '.
'vulnerable to corruption or tampering.');
}
if ($is_corrupt) {
$prompt = pht(
'Corrupting integrity hashes will prevent files from being '.
'accessed. This mode is intended only for development and '.
'debugging.');
}
if ($is_overwrite) {
$prompt = pht(
'Overwriting integrity hashes is dangerous and may mark files '.
'which have been corrupted or tampered with as safe.');
}
if ($prompt) {
$this->logWarn(pht('DANGEROUS'), $prompt);
if (!phutil_console_confirm(pht('Continue anyway?'))) {
throw new PhutilArgumentUsageException(pht('Aborted workflow.'));
}
}
}
$iterator = $this->buildIterator($args);
if (!$iterator) {
throw new PhutilArgumentUsageException(
pht(
'Either specify a list of files to affect, or use "--all" to '.
'affect all files.'));
}
$failure_count = 0;
$total_count = 0;
foreach ($iterator as $file) {
$total_count++;
$display_name = $file->getMonogram();
$old_hash = $file->getIntegrityHash();
if ($is_strip) {
if ($old_hash === null) {
$this->logInfo(
pht('SKIPPED'),
pht(
'File "%s" does not have an integrity hash to strip.',
$display_name));
} else {
$file
->setIntegrityHash(null)
->save();
$this->logWarn(
pht('STRIPPED'),
pht(
'Stripped integrity hash for "%s".',
$display_name));
}
continue;
}
$need_hash = ($is_verify && $old_hash) ||
($is_compute && ($old_hash === null)) ||
($is_corrupt) ||
($is_overwrite);
if ($need_hash) {
try {
$new_hash = $file->newIntegrityHash();
} catch (Exception $ex) {
$failure_count++;
$this->logFail(
pht('ERROR'),
pht(
'Unable to compute integrity hash for file "%s": %s',
$display_name,
$ex->getMessage()));
continue;
}
} else {
$new_hash = null;
}
// NOTE: When running in "corrupt" mode, we only corrupt the hash if
// we're able to compute a valid hash. Some files, like chunked files,
// do not support integrity hashing so corrupting them would create an
// unusual state.
if ($is_corrupt) {
if ($new_hash === null) {
$this->logInfo(
pht('IGNORED'),
pht(
'Storage for file "%s" does not support integrity hashing.',
$display_name));
} else {
$file
->setIntegrityHash('<corrupted>')
->save();
$this->logWarn(
pht('CORRUPTED'),
pht(
'Corrupted integrity hash for file "%s".',
$display_name));
}
continue;
}
if ($is_verify) {
if ($old_hash === null) {
$this->logInfo(
pht('NONE'),
pht(
'File "%s" has no stored integrity hash.',
$display_name));
} else if ($new_hash === null) {
$failure_count++;
$this->logWarn(
pht('UNEXPECTED'),
pht(
'Storage for file "%s" does not support integrity hashing, '.
'but the file has an integrity hash.',
$display_name));
} else if (phutil_hashes_are_identical($old_hash, $new_hash)) {
$this->logOkay(
pht('VALID'),
pht(
'File "%s" has a valid integrity hash.',
$display_name));
} else {
$failure_count++;
$this->logFail(
pht('MISMATCH'),
pht(
'File "%s" has an invalid integrity hash!',
$display_name));
}
continue;
}
if ($is_compute) {
if ($old_hash !== null) {
$this->logInfo(
pht('SKIP'),
pht(
'File "%s" already has an integrity hash.',
$display_name));
} else if ($new_hash === null) {
$this->logInfo(
pht('IGNORED'),
pht(
'Storage for file "%s" does not support integrity hashing.',
$display_name));
} else {
$file
->setIntegrityHash($new_hash)
->save();
$this->logOkay(
pht('COMPUTE'),
pht(
'Computed and stored integrity hash for file "%s".',
$display_name));
}
continue;
}
if ($is_overwrite) {
$same_hash = ($old_hash !== null) &&
($new_hash !== null) &&
phutil_hashes_are_identical($old_hash, $new_hash);
if ($new_hash === null) {
$this->logInfo(
pht('IGNORED'),
pht(
'Storage for file "%s" does not support integrity hashing.',
$display_name));
} else if ($same_hash) {
$this->logInfo(
pht('UNCHANGED'),
pht(
'File "%s" already has the correct integrity hash.',
$display_name));
} else {
$file
->setIntegrityHash($new_hash)
->save();
$this->logOkay(
pht('OVERWRITE'),
pht(
'Overwrote integrity hash for file "%s".',
$display_name));
}
continue;
}
}
if ($failure_count) {
$this->logFail(
pht('FAIL'),
pht(
'Processed %s file(s), encountered %s error(s).',
new PhutilNumber($total_count),
new PhutilNumber($failure_count)));
} else {
$this->logOkay(
pht('DONE'),
pht(
'Processed %s file(s) with no errors.',
new PhutilNumber($total_count)));
}
return 0;
}
}

View file

@ -9,10 +9,13 @@
*
* | name | Human readable filename.
* | authorPHID | User PHID of uploader.
* | ttl | Temporary file lifetime, in seconds.
* | ttl.absolute | Temporary file lifetime as an epoch timestamp.
* | ttl.relative | Temporary file lifetime, relative to now, in seconds.
* | viewPolicy | File visibility policy.
* | isExplicitUpload | Used to show users files they explicitly uploaded.
* | canCDN | Allows the file to be cached and delivered over a CDN.
* | profile | Marks the file as a profile image.
* | format | Internal encoding format.
* | mime-type | Optional, explicit file MIME type.
* | builtin | Optional filename, identifies this as a builtin.
*
@ -24,7 +27,8 @@ final class PhabricatorFile extends PhabricatorFileDAO
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
@ -33,6 +37,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
const METADATA_PARTIAL = 'partial';
const METADATA_PROFILE = 'profile';
const METADATA_STORAGE = 'storage';
const METADATA_INTEGRITY = 'integrity';
protected $name;
protected $mimeType;
@ -90,7 +95,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes40?',
'contentHash' => 'bytes64?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
@ -193,51 +198,19 @@ final class PhabricatorFile extends PhabricatorFileDAO
}
/**
* 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',
idx($params, 'name'),
self::hashFileContent($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = self::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFileFromContentHash($hash, array $params) {
// Check to see if a file with same contentHash exist
if ($hash === null) {
return null;
}
// Check to see if a file with same hash already exists.
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if (!$file) {
return null;
}
if ($file) {
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
@ -264,9 +237,6 @@ final class PhabricatorFile extends PhabricatorFileDAO
return $new_file;
}
return $file;
}
public static function newChunkedFile(
PhabricatorFileStorageEngine $engine,
$length,
@ -355,15 +325,18 @@ final class PhabricatorFile extends PhabricatorFileDAO
$data_handle = null;
$engine_identifier = null;
$integrity_hash = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$result = $file->writeToEngine(
$engine,
$data,
$params);
list($engine_identifier, $data_handle, $integrity_hash) = $result;
// We stored the file somewhere so stop trying to write it to other
// places.
break;
@ -387,11 +360,15 @@ final class PhabricatorFile extends PhabricatorFileDAO
}
$file->setByteSize(strlen($data));
$file->setContentHash(self::hashFileContent($data));
$hash = self::hashFileContent($data);
$file->setContentHash($hash);
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
$file->setIntegrityHash($integrity_hash);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
@ -413,11 +390,13 @@ final class PhabricatorFile extends PhabricatorFileDAO
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
$file = self::newFileFromContentHash($hash, $params);
if ($hash !== null) {
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
}
return self::buildFromFileData($data, $params);
}
@ -436,7 +415,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
list($new_identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
$engine,
$data,
$params);
@ -447,6 +426,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->setIntegrityHash($integrity_hash);
$this->save();
if (!$make_copy) {
@ -513,14 +493,14 @@ final class PhabricatorFile extends PhabricatorFileDAO
$engine_class = get_class($engine);
$key = $this->getStorageFormat();
$format = id(clone PhabricatorFileStorageFormat::requireFormat($key))
->setFile($this);
$format = $this->newStorageFormat();
$data_iterator = array($data);
$formatted_iterator = $format->newWriteIterator($data_iterator);
$formatted_data = $this->loadDataFromIterator($formatted_iterator);
$integrity_hash = $engine->newIntegrityHash($formatted_data, $format);
$data_handle = $engine->writeFile($formatted_data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
@ -545,7 +525,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
$engine_identifier));
}
return array($engine_identifier, $data_handle);
return array($engine_identifier, $data_handle, $integrity_hash);
}
@ -744,9 +724,18 @@ final class PhabricatorFile extends PhabricatorFileDAO
}
}
public static function hashFileContent($data) {
return sha1($data);
// NOTE: Hashing can fail if the algorithm isn't available in the current
// build of PHP. It's fine if we're unable to generate a content hash:
// it just means we'll store extra data when users upload duplicate files
// instead of being able to deduplicate it.
$hash = hash('sha256', $data, $raw_output = false);
if ($hash === false) {
return null;
}
return $hash;
}
public function loadFileData() {
@ -764,16 +753,21 @@ final class PhabricatorFile extends PhabricatorFileDAO
*/
public function getFileDataIterator($begin = null, $end = null) {
$engine = $this->instantiateStorageEngine();
$raw_iterator = $engine->getRawFileDataIterator($this, $begin, $end);
$key = $this->getStorageFormat();
$format = $this->newStorageFormat();
$format = id(clone PhabricatorFileStorageFormat::requireFormat($key))
->setFile($this);
$iterator = $engine->getRawFileDataIterator(
$this,
$begin,
$end,
$format);
return $format->newReadIterator($raw_iterator);
return $iterator;
}
public function getURI() {
return $this->getInfoURI();
}
public function getViewURI() {
if (!$this->getPHID()) {
@ -1106,7 +1100,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
$params = array(
'name' => $builtin->getBuiltinDisplayName(),
'ttl' => time() + (60 * 60 * 24 * 7),
'ttl.relative' => phutil_units('7 days in seconds'),
'canCDN' => true,
'builtin' => $key,
);
@ -1231,6 +1225,30 @@ final class PhabricatorFile extends PhabricatorFileDAO
return $this;
}
public function setIntegrityHash($integrity_hash) {
$this->metadata[self::METADATA_INTEGRITY] = $integrity_hash;
return $this;
}
public function getIntegrityHash() {
return idx($this->metadata, self::METADATA_INTEGRITY);
}
public function newIntegrityHash() {
$engine = $this->instantiateStorageEngine();
if ($engine->isChunkEngine()) {
return null;
}
$format = $this->newStorageFormat();
$storage_handle = $this->getStorageHandle();
$data = $engine->readFile($storage_handle);
return $engine->newIntegrityHash($data, $format);
}
/**
* Write the policy edge between this file and some object.
*
@ -1276,14 +1294,68 @@ final class PhabricatorFile extends PhabricatorFileDAO
* @return this
*/
private function readPropertiesFromParameters(array $params) {
PhutilTypeSpec::checkMap(
$params,
array(
'name' => 'optional string',
'authorPHID' => 'optional string',
'ttl.relative' => 'optional int',
'ttl.absolute' => 'optional int',
'viewPolicy' => 'optional string',
'isExplicitUpload' => 'optional bool',
'canCDN' => 'optional bool',
'profile' => 'optional bool',
'format' => 'optional string|PhabricatorFileStorageFormat',
'mime-type' => 'optional string',
'builtin' => 'optional string',
'storageEngines' => 'optional list<PhabricatorFileStorageEngine>',
));
$file_name = idx($params, 'name');
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$file_ttl = idx($params, 'ttl');
$this->setTtl($file_ttl);
$absolute_ttl = idx($params, 'ttl.absolute');
$relative_ttl = idx($params, 'ttl.relative');
if ($absolute_ttl !== null && $relative_ttl !== null) {
throw new Exception(
pht(
'Specify an absolute TTL or a relative TTL, but not both.'));
} else if ($absolute_ttl !== null) {
if ($absolute_ttl < PhabricatorTime::getNow()) {
throw new Exception(
pht(
'Absolute TTL must be in the present or future, but TTL "%s" '.
'is in the past.',
$absolute_ttl));
}
$this->setTtl($absolute_ttl);
} else if ($relative_ttl !== null) {
if ($relative_ttl < 0) {
throw new Exception(
pht(
'Relative TTL must be zero or more seconds, but "%s" is '.
'negative.',
$relative_ttl));
}
$max_relative = phutil_units('365 days in seconds');
if ($relative_ttl > $max_relative) {
throw new Exception(
pht(
'Relative TTL must not be more than "%s" seconds, but TTL '.
'"%s" was specified.',
$max_relative,
$relative_ttl));
}
$absolute_ttl = PhabricatorTime::getNow() + $relative_ttl;
$this->setTtl($absolute_ttl);
}
$view_policy = idx($params, 'viewPolicy');
if ($view_policy) {
@ -1345,6 +1417,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
return $this->assertAttachedKey($this->transforms, $key);
}
public function newStorageFormat() {
$key = $this->getStorageFormat();
$template = PhabricatorFileStorageFormat::requireFormat($key);
$format = id(clone $template)
->setFile($this);
return $format;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
@ -1465,4 +1547,37 @@ final class PhabricatorFile extends PhabricatorFileDAO
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the file.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dataURI')
->setType('string')
->setDescription(pht('Download URI for the file data.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('size')
->setType('int')
->setDescription(pht('File size, in bytes.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'dataURI' => $this->getCDNURI(),
'size' => (int)$this->getByteSize(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}

View file

@ -1,9 +1,7 @@
<?php
final class PhabricatorFileTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'file:name';
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'file';
@ -17,69 +15,8 @@ final class PhabricatorFileTransaction
return new PhabricatorFileTransactionComment();
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
return pht(
'%s updated the name for this file from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$old,
$new);
break;
public function getBaseTransactionClass() {
return 'PhabricatorFileTransactionType';
}
return parent::getTitle();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$type = $this->getTransactionType();
switch ($type) {
case self::TYPE_NAME:
return pht(
'%s updated the name of %s from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$old,
$new);
break;
}
return parent::getTitleForFeed();
}
public function getIcon() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
return 'fa-pencil';
}
return parent::getIcon();
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_NAME:
return PhabricatorTransactions::COLOR_BLUE;
}
return parent::getColor();
}
}

View file

@ -32,7 +32,7 @@ final class PhabricatorFileTestCase extends PhabricatorTestCase {
// First, change the name: this should not scramble the secret.
$xactions = array();
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorFileTransaction::TYPE_NAME)
->setTransactionType(PhabricatorFileNameTransaction::TRANSACTIONTYPE)
->setNewValue('test.dat2');
$engine = id(new PhabricatorFileEditor())
@ -301,6 +301,11 @@ final class PhabricatorFileTestCase extends PhabricatorTestCase {
$data = Filesystem::readRandomCharacters(64);
$hash = PhabricatorFile::hashFileContent($data);
if ($hash === null) {
$this->assertSkipped(pht('File content hashing is not available.'));
}
$params = array(
'name' => 'test.dat',
'storageEngines' => array(
@ -370,11 +375,11 @@ final class PhabricatorFileTestCase extends PhabricatorTestCase {
$data = Filesystem::readRandomCharacters(64);
$ttl = (time() + 60 * 60 * 24);
$ttl = (PhabricatorTime::getNow() + phutil_units('24 hours in seconds'));
$params = array(
'name' => 'test.dat',
'ttl' => ($ttl),
'ttl.absolute' => $ttl,
'storageEngines' => array(
$engine,
),

View file

@ -4,7 +4,7 @@ abstract class PhabricatorFileUploadSource
extends Phobject {
private $name;
private $ttl;
private $relativeTTL;
private $viewPolicy;
private $rope;
@ -22,13 +22,13 @@ abstract class PhabricatorFileUploadSource
return $this->name;
}
public function setTTL($ttl) {
$this->ttl = $ttl;
public function setRelativeTTL($relative_ttl) {
$this->relativeTTL = $relative_ttl;
return $this;
}
public function getTTL() {
return $this->ttl;
public function getRelativeTTL() {
return $this->relativeTTL;
}
public function setViewPolicy($view_policy) {
@ -214,7 +214,7 @@ abstract class PhabricatorFileUploadSource
private function getNewFileParameters() {
return array(
'name' => $this->getName(),
'ttl' => $this->getTTL(),
'ttl.relative' => $this->getRelativeTTL(),
'viewPolicy' => $this->getViewPolicy(),
);
}

View file

@ -0,0 +1,55 @@
<?php
final class PhabricatorFileNameTransaction
extends PhabricatorFileTransactionType {
const TRANSACTIONTYPE = 'file:name';
public function generateOldValue($object) {
return $object->getName();
}
public function applyInternalEffects($object, $value) {
$object->setName($value);
}
public function getTitle() {
return pht(
'%s updated the name for this file from "%s" to "%s".',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function getTitleForFeed() {
return pht(
'%s updated the name of %s from "%s" to "%s".',
$this->renderAuthor(),
$this->renderObject(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
$errors[] = $this->newRequiredError(pht('Files must have a name.'));
}
$max_length = $object->getColumnMaximumByteLength('name');
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
pht(
'File names must not be longer than %s character(s).',
new PhutilNumber($max_length)));
}
}
return $errors;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorFileTransactionType
extends PhabricatorModularTransactionType {}

View file

@ -6,7 +6,7 @@ final class HarbormasterExternalBuildStepGroup
const GROUPKEY = 'harbormaster.external';
public function getGroupName() {
return pht('Interacting with External Build Sytems');
return pht('Interacting with External Build Systems');
}
public function getGroupOrder() {

View file

@ -39,8 +39,8 @@ final class LegalpadDocumentBody extends LegalpadDAO
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'LEGB:'.$hash;
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {

View file

@ -301,9 +301,8 @@ final class ManiphestTask extends ManiphestDAO
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}

View file

@ -201,7 +201,7 @@ abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver {
public static function computeMailHash($mail_key, $phid) {
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid);
$hash = PhabricatorHash::weakDigest($mail_key.$global_mail_key.$phid);
return substr($hash, 0, 16);
}

View file

@ -79,7 +79,6 @@ final class PhabricatorPasteContentTransaction
'mime-type' => 'text/plain; charset=utf-8',
'authorPHID' => $actor->getPHID(),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
'editPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
}

View file

@ -38,7 +38,7 @@ final class PhabricatorPeopleLdapController
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Import Ldap Users'),
pht('Import LDAP Users'),
$this->getApplicationURI('/ldap/'));
$nav = $this->buildSideNavView();
@ -56,7 +56,7 @@ final class PhabricatorPeopleLdapController
}
return $this->newPage()
->setTitle(pht('Import Ldap Users'))
->setTitle(pht('Import LDAP Users'))
->setCrumbs($crumbs)
->setNavigation($nav);
}

View file

@ -389,7 +389,7 @@ final class PhabricatorUser
// Generate a token hash to mitigate BREACH attacks against SSL. See
// discussion in T3684.
$token = $this->getRawCSRFToken();
$hash = PhabricatorHash::digest($token, $salt);
$hash = PhabricatorHash::weakDigest($token, $salt);
return self::CSRF_BREACH_PREFIX.$salt.substr(
$hash, 0, self::CSRF_TOKEN_LENGTH);
}
@ -435,7 +435,7 @@ final class PhabricatorUser
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getRawCSRFToken($ii);
$digest = PhabricatorHash::digest($valid, $salt);
$digest = PhabricatorHash::weakDigest($valid, $salt);
$digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH);
if (phutil_hashes_are_identical($digest, $token)) {
return true;
@ -459,7 +459,7 @@ final class PhabricatorUser
$time_block = floor($epoch / $frequency);
$vec = $vec.$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
return substr(PhabricatorHash::weakDigest($vec), 0, $len);
}
public function getUserProfile() {

View file

@ -289,8 +289,8 @@ final class PhameBlog extends PhameDAO
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}

View file

@ -241,8 +241,8 @@ final class PhamePost extends PhameDAO
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {

View file

@ -84,8 +84,8 @@ final class PholioImage extends PholioDAO
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'M:'.$hash;
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {

View file

@ -217,8 +217,8 @@ final class PholioMock extends PholioDAO
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'M:'.$hash;
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {

View file

@ -174,12 +174,15 @@ final class PhragmentGetPatchConduitAPIMethod
unset($patches[$key]['fileOld']);
unset($patches[$key]['fileNew']);
$file = PhabricatorFile::buildFromFileDataOrHash(
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => 'patch.dmp',
'ttl' => time() + 60 * 60 * 24,
'ttl.relative' => phutil_units('24 hours in seconds'),
));
unset($unguarded);
$patches[$key]['patchURI'] = $file->getDownloadURI();
}

View file

@ -78,15 +78,15 @@ final class PhragmentPatchController extends PhragmentController {
$return = $request->getStr('return');
}
$result = PhabricatorFile::buildFromFileDataOrHash(
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$result = PhabricatorFile::newFromFileData(
$patch,
array(
'name' => $name,
'mime-type' => 'text/plain',
'ttl' => time() + 60 * 60 * 24,
'ttl.relative' => phutil_units('24 hours in seconds'),
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$result->attachToObject($version_b->getFragmentPHID());
unset($unguarded);

View file

@ -100,14 +100,15 @@ final class PhragmentZIPController extends PhragmentController {
}
$data = Filesystem::readFile((string)$temp);
$file = PhabricatorFile::buildFromFileDataOrHash(
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $zip_name,
'ttl' => time() + 60 * 60 * 24,
'ttl.relative' => phutil_units('24 hours in seconds'),
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file->attachToObject($fragment->getPHID());
unset($unguarded);

View file

@ -68,12 +68,8 @@ final class PhrictionContent extends PhrictionDAO
* @task markup
*/
public function getMarkupFieldKey($field) {
if ($this->shouldUseMarkupCache($field)) {
$id = $this->getID();
} else {
$id = PhabricatorHash::digest($this->getMarkupText($field));
}
return "phriction:{$field}:{$id}";
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}

View file

@ -136,9 +136,8 @@ final class PonderAnswer extends PonderDAO
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:A{$id}:{$field}:{$hash}";
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function getMarkupText($field) {

View file

@ -155,9 +155,8 @@ final class PonderQuestion extends PonderDAO
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:Q{$id}:{$field}:{$hash}";
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function getMarkupText($field) {

View file

@ -236,12 +236,14 @@ abstract class ReleephFieldSpecification
}
final public function getMarkupFieldKey($field) {
return sprintf(
$content = sprintf(
'%s:%s:%s:%s',
$this->getReleephRequest()->getPHID(),
$this->getStorageKey(),
$field,
PhabricatorHash::digest($this->getMarkupText($field)));
$this->getMarkupText($field));
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
final public function newMarkupEngine($field) {

View file

@ -68,6 +68,7 @@ final class PhabricatorRepositoryPullLocalDaemon
$retry_after = array();
$min_sleep = 15;
$max_sleep = phutil_units('5 minutes in seconds');
$max_futures = 4;
$futures = array();
$queue = array();
@ -228,7 +229,7 @@ final class PhabricatorRepositoryPullLocalDaemon
continue;
}
$should_hibernate = $this->waitForUpdates($min_sleep, $retry_after);
$should_hibernate = $this->waitForUpdates($max_sleep, $retry_after);
if ($should_hibernate) {
break;
}

View file

@ -634,6 +634,10 @@ final class PhabricatorRepositoryEditor
->synchronizeWorkingCopyAfterCreation();
}
$object->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
null);
return $xactions;
}

View file

@ -48,7 +48,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($password_type))
->withTokenCodes(array(PhabricatorHash::digest($key)))
->withTokenCodes(array(PhabricatorHash::weakDigest($key)))
->withExpired(false)
->executeOne();
}

View file

@ -44,7 +44,7 @@ final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel {
->withPHIDs($identity_phids)
->execute();
$current_key = PhabricatorHash::digest(
$current_key = PhabricatorHash::weakDigest(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION));
$rows = array();

View file

@ -20,6 +20,7 @@ final class PhabricatorRobotsController extends PhabricatorController {
$out[] = 'User-Agent: *';
$out[] = 'Disallow: /diffusion/';
$out[] = 'Disallow: /source/';
// Add a small crawl delay (number of seconds between requests) for spiders
// which respect it. The intent here is to prevent spiders from affecting

View file

@ -1149,12 +1149,20 @@ abstract class PhabricatorApplicationTransaction
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created %s in the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted %s from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_EDGE:
$new = ipull($new, 'dst');
$old = ipull($old, 'dst');

View file

@ -31,4 +31,40 @@ abstract class PhabricatorManagementWorkflow extends PhutilArgumentWorkflow {
PhabricatorConsoleContentSource::SOURCECONST);
}
protected function logInfo($label, $message) {
$this->logRaw(
tsprintf(
"**<bg:blue> %s </bg>** %s\n",
$label,
$message));
}
protected function logOkay($label, $message) {
$this->logRaw(
tsprintf(
"**<bg:green> %s </bg>** %s\n",
$label,
$message));
}
protected function logWarn($label, $message) {
$this->logRaw(
tsprintf(
"**<bg:yellow> %s </bg>** %s\n",
$label,
$message));
}
protected function logFail($label, $message) {
$this->logRaw(
tsprintf(
"**<bg:red> %s </bg>** %s\n",
$label,
$message));
}
private function logRaw($message) {
fprintf(STDERR, '%s', $message);
}
}

View file

@ -694,4 +694,19 @@ final class PhabricatorMarkupEngine extends Phobject {
->execute();
}
public static function digestRemarkupContent($object, $content) {
$parts = array();
$parts[] = get_class($object);
if ($object instanceof PhabricatorLiskDAO) {
$parts[] = $object->getID();
}
$parts[] = $content;
$message = implode("\n", $parts);
return PhabricatorHash::digestWithNamedKey($message, 'remarkup');
}
}

View file

@ -5,12 +5,15 @@ final class PhabricatorHash extends Phobject {
const INDEX_DIGEST_LENGTH = 12;
/**
* Digest a string for general use, including use which relates to security.
* Digest a string using HMAC+SHA1.
*
* Because a SHA1 collision is now known, this method should be considered
* weak. Callers should prefer @{method:digestWithNamedKey}.
*
* @param string Input string.
* @return string 32-byte hexidecimal SHA1+HMAC hash.
*/
public static function digest($string, $key = null) {
public static function weakDigest($string, $key = null) {
if ($key === null) {
$key = PhabricatorEnv::getEnvConfig('security.hmac-key');
}
@ -37,7 +40,7 @@ final class PhabricatorHash extends Phobject {
}
for ($ii = 0; $ii < 1000; $ii++) {
$result = self::digest($result, $salt);
$result = self::weakDigest($result, $salt);
}
return $result;
@ -139,5 +142,89 @@ final class PhabricatorHash extends Phobject {
return $prefix.'-'.$hash;
}
public static function digestWithNamedKey($message, $key_name) {
$key_bytes = self::getNamedHMACKey($key_name);
return self::digestHMACSHA256($message, $key_bytes);
}
public static function digestHMACSHA256($message, $key) {
if (!strlen($key)) {
throw new Exception(
pht('HMAC-SHA256 requires a nonempty key.'));
}
$result = hash_hmac('sha256', $message, $key, $raw_output = false);
if ($result === false) {
throw new Exception(
pht('Unable to compute HMAC-SHA256 digest of message.'));
}
return $result;
}
/* -( HMAC Key Management )------------------------------------------------ */
private static function getNamedHMACKey($hmac_name) {
$cache = PhabricatorCaches::getImmutableCache();
$cache_key = "hmac.key({$hmac_name})";
$hmac_key = $cache->getKey($cache_key);
if (!strlen($hmac_key)) {
$hmac_key = self::readHMACKey($hmac_name);
if ($hmac_key === null) {
$hmac_key = self::newHMACKey($hmac_name);
self::writeHMACKey($hmac_name, $hmac_key);
}
$cache->setKey($cache_key, $hmac_key);
}
// The "hex2bin()" function doesn't exist until PHP 5.4.0 so just
// implement it inline.
$result = '';
for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) {
$result .= pack('H*', substr($hmac_key, $ii, 2));
}
return $result;
}
private static function newHMACKey($hmac_name) {
$hmac_key = Filesystem::readRandomBytes(64);
return bin2hex($hmac_key);
}
private static function writeHMACKey($hmac_name, $hmac_key) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthHMACKey())
->setKeyName($hmac_name)
->setKeyValue($hmac_key)
->save();
unset($unguarded);
}
private static function readHMACKey($hmac_name) {
$table = new PhabricatorAuthHMACKey();
$conn = $table->establishConnection('r');
$row = queryfx_one(
$conn,
'SELECT keyValue FROM %T WHERE keyName = %s',
$table->getTableName(),
$hmac_name);
if (!$row) {
return null;
}
return $row['keyValue'];
}
}

View file

@ -0,0 +1,40 @@
<?php
final class PhabricatorHMACTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testHMACKeyGeneration() {
$input = 'quack';
$hash_1 = PhabricatorHash::digestWithNamedKey($input, 'test');
$hash_2 = PhabricatorHash::digestWithNamedKey($input, 'test');
$this->assertEqual($hash_1, $hash_2);
}
public function testSHA256Hashing() {
$input = 'quack';
$key = 'duck';
$expect =
'5274473dc34fc61bd7a6a5ff258e6505'.
'4b26644fb7a272d74f276ab677361b9a';
$hash = PhabricatorHash::digestHMACSHA256($input, $key);
$this->assertEqual($expect, $hash);
$input = 'The quick brown fox jumps over the lazy dog';
$key = 'key';
$expect =
'f7bc83f430538424b13298e6aa6fb143'.
'ef4d59a14946175997479dbc2d1a3cd8';
$hash = PhabricatorHash::digestHMACSHA256($input, $key);
$this->assertEqual($expect, $hash);
}
}