Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php
index 3656fe6507..ebdaeacd0a 100644
--- a/src/applications/differential/storage/DifferentialChangeset.php
+++ b/src/applications/differential/storage/DifferentialChangeset.php
@@ -1,263 +1,263 @@
<?php
final class DifferentialChangeset
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $diffID;
protected $oldFile;
protected $filename;
protected $awayPaths;
protected $changeType;
protected $fileType;
protected $metadata;
protected $oldProperties;
protected $newProperties;
protected $addLines;
protected $delLines;
private $unsavedHunks = array();
private $hunks = self::ATTACHABLE;
private $diff = self::ATTACHABLE;
const TABLE_CACHE = 'differential_changeset_parse_cache';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
'oldProperties' => self::SERIALIZATION_JSON,
'newProperties' => self::SERIALIZATION_JSON,
'awayPaths' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'oldFile' => 'bytes?',
'filename' => 'bytes',
'changeType' => 'uint32',
'fileType' => 'uint32',
'addLines' => 'uint32',
'delLines' => 'uint32',
// T6203/NULLABILITY
// These should all be non-nullable, and store reasonable default
// JSON values if empty.
'awayPaths' => 'text?',
'metadata' => 'text?',
'oldProperties' => 'text?',
'newProperties' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'diffID' => array(
'columns' => array('diffID'),
),
),
) + parent::getConfiguration();
}
public function getAffectedLineCount() {
return $this->getAddLines() + $this->getDelLines();
}
public function attachHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$this->hunks = $hunks;
return $this;
}
public function getHunks() {
return $this->assertAttached($this->hunks);
}
public function getDisplayFilename() {
$name = $this->getFilename();
if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$name .= '/';
}
return $name;
}
public function getOwnersFilename() {
// TODO: For Subversion, we should adjust these paths to be relative to
// the repository root where possible.
$path = $this->getFilename();
if (!isset($path[0])) {
return '/';
}
if ($path[0] != '/') {
$path = '/'.$path;
}
return $path;
}
public function addUnsavedHunk(DifferentialHunk $hunk) {
if ($this->hunks === self::ATTACHABLE) {
$this->hunks = array();
}
$this->hunks[] = $hunk;
$this->unsavedHunks[] = $hunk;
return $this;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedHunks as $hunk) {
$hunk->setChangesetID($this->getID());
$hunk->save();
}
$this->saveTransaction();
return $ret;
}
public function delete() {
$this->openTransaction();
$modern_hunks = id(new DifferentialModernHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($modern_hunks as $modern_hunk) {
$modern_hunk->delete();
}
$this->unsavedHunks = array();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE id = %d',
self::TABLE_CACHE,
$this->getID());
$ret = parent::delete();
$this->saveTransaction();
return $ret;
}
public function getSortKey() {
$sort_key = $this->getFilename();
// Sort files with ".h" in them first, so headers (.h, .hpp) come before
// implementations (.c, .cpp, .cs).
$sort_key = str_replace('.h', '.!h', $sort_key);
return $sort_key;
}
public function makeNewFile() {
$file = mpull($this->getHunks(), 'makeNewFile');
return implode('', $file);
}
public function makeOldFile() {
$file = mpull($this->getHunks(), 'makeOldFile');
return implode('', $file);
}
public function makeChangesWithContext($num_lines = 3) {
$with_context = array();
foreach ($this->getHunks() as $hunk) {
$context = array();
$changes = explode("\n", $hunk->getChanges());
foreach ($changes as $l => $line) {
$type = substr($line, 0, 1);
if ($type == '+' || $type == '-') {
$context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
}
}
$with_context[] = array_intersect_key($changes, $context);
}
return array_mergev($with_context);
}
public function getAnchorName() {
- return 'change-'.PhabricatorHash::digestForIndex($this->getFilename());
+ return 'change-'.PhabricatorHash::digestForAnchor($this->getFilename());
}
public function getAbsoluteRepositoryPath(
PhabricatorRepository $repository = null,
DifferentialDiff $diff = null) {
$base = '/';
if ($diff && $diff->getSourceControlPath()) {
$base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
}
$path = $this->getFilename();
$path = rtrim($base, '/').'/'.ltrim($path, '/');
$svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
if ($repository && $repository->getVersionControlSystem() == $svn) {
$prefix = $repository->getDetail('remote-uri');
$prefix = id(new PhutilURI($prefix))->getPath();
if (!strncmp($path, $prefix, strlen($prefix))) {
$path = substr($path, strlen($prefix));
}
$path = '/'.ltrim($path, '/');
}
return $path;
}
public function getWhitespaceMatters() {
$config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters');
foreach ($config as $regexp) {
if (preg_match($regexp, $this->getFilename())) {
return true;
}
}
return false;
}
public function attachDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function getDiff() {
return $this->assertAttached($this->diff);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getDiff()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getDiff()->hasAutomaticCapability($capability, $viewer);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$hunks = id(new DifferentialModernHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
foreach ($hunks as $hunk) {
$engine->destroyObject($hunk);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
index 4cd1abcf15..ce48b0966b 100644
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -1,212 +1,269 @@
<?php
final class PhabricatorHash extends Phobject {
const INDEX_DIGEST_LENGTH = 12;
+ const ANCHOR_DIGEST_LENGTH = 12;
/**
* 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 hexadecimal SHA1+HMAC hash.
*/
public static function weakDigest($string, $key = null) {
if ($key === null) {
$key = PhabricatorEnv::getEnvConfig('security.hmac-key');
}
if (!$key) {
throw new Exception(
pht(
"Set a '%s' in your Phabricator configuration!",
'security.hmac-key'));
}
return hash_hmac('sha1', $string, $key);
}
/**
* Digest a string for use in, e.g., a MySQL index. This produces a short
* (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,
* which is generally safe in most contexts (notably, URLs).
*
* This method emphasizes compactness, and should not be used for security
* related hashing (for general purpose hashing, see @{method:digest}).
*
* @param string Input string.
- * @return string 12-byte, case-sensitive alphanumeric hash of the string
- * which
+ * @return string 12-byte, case-sensitive, mostly-alphanumeric hash of
+ * the string.
*/
public static function digestForIndex($string) {
$hash = sha1($string, $raw_output = true);
static $map;
if ($map === null) {
$map = '0123456789'.
'abcdefghij'.
'klmnopqrst'.
'uvwxyzABCD'.
'EFGHIJKLMN'.
'OPQRSTUVWX'.
'YZ._';
}
$result = '';
for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) {
$result .= $map[(ord($hash[$ii]) & 0x3F)];
}
return $result;
}
+ /**
+ * Digest a string for use in HTML page anchors. This is similar to
+ * @{method:digestForIndex} but produces purely alphanumeric output.
+ *
+ * This tries to be mostly compatible with the index digest to limit how
+ * much stuff we're breaking by switching to it. For additional discussion,
+ * see T13045.
+ *
+ * @param string Input string.
+ * @return string 12-byte, case-sensitive, purely-alphanumeric hash of
+ * the string.
+ */
+ public static function digestForAnchor($string) {
+ $hash = sha1($string, $raw_output = true);
+
+ static $map;
+ if ($map === null) {
+ $map = '0123456789'.
+ 'abcdefghij'.
+ 'klmnopqrst'.
+ 'uvwxyzABCD'.
+ 'EFGHIJKLMN'.
+ 'OPQRSTUVWX'.
+ 'YZ';
+ }
+
+ $result = '';
+ $accum = 0;
+ $map_size = strlen($map);
+ for ($ii = 0; $ii < self::ANCHOR_DIGEST_LENGTH; $ii++) {
+ $byte = ord($hash[$ii]);
+ $low_bits = ($byte & 0x3F);
+ $accum = ($accum + $byte) % $map_size;
+
+ if ($low_bits < $map_size) {
+ // If an index digest would produce any alphanumeric character, just
+ // use that character. This means that these digests are the same as
+ // digests created with "digestForIndex()" in all positions where the
+ // output character is some character other than "." or "_".
+ $result .= $map[$low_bits];
+ } else {
+ // If an index digest would produce a non-alphumeric character ("." or
+ // "_"), pick an alphanumeric character instead. We accumulate an
+ // index into the alphanumeric character list to try to preserve
+ // entropy here. We could use this strategy for all bytes instead,
+ // but then these digests would differ from digests created with
+ // "digestForIndex()" in all positions, instead of just a small number
+ // of positions.
+ $result .= $map[$accum];
+ }
+ }
+
+ return $result;
+ }
+
+
public static function digestToRange($string, $min, $max) {
if ($min > $max) {
throw new Exception(pht('Maximum must be larger than minimum.'));
}
if ($min == $max) {
return $min;
}
$hash = sha1($string, $raw_output = true);
// Make sure this ends up positive, even on 32-bit machines.
$value = head(unpack('L', $hash)) & 0x7FFFFFFF;
return $min + ($value % (1 + $max - $min));
}
/**
* Shorten a string to a maximum byte length in a collision-resistant way
* while retaining some degree of human-readability.
*
* This function converts an input string into a prefix plus a hash. For
* example, a very long string beginning with "crabapplepie..." might be
* digested to something like "crabapp-N1wM1Nz3U84k".
*
* This allows the maximum length of identifiers to be fixed while
* maintaining a high degree of collision resistance and a moderate degree
* of human readability.
*
* @param string The string to shorten.
* @param int Maximum length of the result.
* @return string String shortened in a collision-resistant way.
*/
public static function digestToLength($string, $length) {
// We need at least two more characters than the hash length to fit in a
// a 1-character prefix and a separator.
$min_length = self::INDEX_DIGEST_LENGTH + 2;
if ($length < $min_length) {
throw new Exception(
pht(
'Length parameter in %s must be at least %s, '.
'but %s was provided.',
'digestToLength()',
new PhutilNumber($min_length),
new PhutilNumber($length)));
}
// We could conceivably return the string unmodified if it's shorter than
// the specified length. Instead, always hash it. This makes the output of
// the method more recognizable and consistent (no surprising new behavior
// once you hit a string longer than `$length`) and prevents an attacker
// who can control the inputs from intentionally using the hashed form
// of a string to cause a collision.
$hash = self::digestForIndex($string);
$prefix = substr($string, 0, ($length - ($min_length - 1)));
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'];
}
}
diff --git a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
index b89515701d..008270e767 100644
--- a/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
+++ b/src/infrastructure/util/__tests__/PhabricatorHashTestCase.php
@@ -1,55 +1,98 @@
<?php
final class PhabricatorHashTestCase extends PhabricatorTestCase {
public function testHashForIndex() {
$map = array(
'dog' => 'Aliif7Qjd5ct',
'cat' => 'toudDsue3Uv8',
'rat' => 'RswaKgTjqOuj',
'bat' => 'rAkJKyX4YdYm',
);
foreach ($map as $input => $expect) {
$this->assertEqual(
$expect,
PhabricatorHash::digestForIndex($input),
pht('Input: %s', $input));
}
// Test that the encoding produces 6 bits of entropy per byte.
$entropy = array(
'dog',
'cat',
'rat',
'bat',
'dig',
'fig',
'cot',
'cut',
'fog',
'rig',
'rug',
'dug',
'mat',
'pat',
'eat',
'tar',
'pot',
);
$seen = array();
foreach ($entropy as $input) {
$chars = preg_split('//', PhabricatorHash::digestForIndex($input));
foreach ($chars as $char) {
$seen[$char] = true;
}
}
$this->assertEqual(
(1 << 6),
count($seen),
pht('Distinct characters in hash of: %s', $input));
}
+ public function testHashForAnchor() {
+ $map = array(
+ // For inputs with no "." or "_" in the output, digesting for an index
+ // or an anchor should be the same.
+ 'dog' => array(
+ 'Aliif7Qjd5ct',
+ 'Aliif7Qjd5ct',
+ ),
+ // When an output would contain "." or "_", it should be replaced with
+ // an alphanumeric character in those positions instead.
+ 'fig' => array(
+ 'OpB9tY4i.MOX',
+ 'OpB9tY4imMOX',
+ ),
+ 'cot' => array(
+ '_iF26XU_PsVY',
+ '3iF26XUkPsVY',
+ ),
+ // The replacement characters are well-distributed and generally keep
+ // the entropy of the output high: here, "_" characters in the initial
+ // positions of the digests of "cot" (above) and "dug" (this test) have
+ // different outputs.
+ 'dug' => array(
+ '_XuQnp0LUlUW',
+ '7XuQnp0LUlUW',
+ ),
+ );
+
+ foreach ($map as $input => $expect) {
+ list($expect_index, $expect_anchor) = $expect;
+
+ $this->assertEqual(
+ $expect_index,
+ PhabricatorHash::digestForIndex($input),
+ pht('Index digest of "%s".', $input));
+
+ $this->assertEqual(
+ $expect_anchor,
+ PhabricatorHash::digestForAnchor($input),
+ pht('Anchor digest of "%s".', $input));
+ }
+ }
+
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Apr 29, 5:40 PM (1 d, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
108462
Default Alt Text
(18 KB)

Event Timeline