diff --git a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php index 7eca716e34..4e461ec3f5 100644 --- a/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php +++ b/src/applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php @@ -1,444 +1,445 @@ icon = $icon; return $this; } public function getIcon() { return $this->icon; } public function setColor($color) { $this->color = $color; return $this; } public function getColor() { return $this->color; } public function setBorder($border) { $this->border = $border; return $this; } public function getBorder() { return $this->border; } public function getBuiltinFileKey() { $icon = $this->getIcon(); $color = $this->getColor(); $border = implode(',', $this->getBorder()); $desc = "compose(icon={$icon}, color={$color}, border={$border}"; $hash = PhabricatorHash::digestToLength($desc, 40); return "builtin:{$hash}"; } public function getBuiltinDisplayName() { $icon = $this->getIcon(); $color = $this->getColor(); $border = implode(',', $this->getBorder()); return "{$icon}-{$color}-{$border}.png"; } public function loadBuiltinFileData() { return $this->composeImage( $this->getColor(), $this->getIcon(), $this->getBorder()); } private function composeImage($color, $image, $border) { $color_const = hexdec(trim($color, '#')); $true_border = self::rgba2gd($border); $image_map = self::getImageMap(); $data = Filesystem::readFile($image_map[$image]); $img = imagecreatefromstring($data); // 4 pixel border at 50x50, 32 pixel border at 400x400 $canvas = imagecreatetruecolor(400, 400); $image_fill = imagefill($canvas, 0, 0, $color_const); if (!$image_fill) { throw new Exception( pht('Failed to save builtin avatar image data (imagefill).')); } $border_thickness = imagesetthickness($canvas, 64); if (!$border_thickness) { throw new Exception( pht('Failed to save builtin avatar image data (imagesetthickness).')); } $image_rectangle = imagerectangle($canvas, 0, 0, 400, 400, $true_border); if (!$image_rectangle) { throw new Exception( pht('Failed to save builtin avatar image data (imagerectangle).')); } $image_copy = imagecopy($canvas, $img, 0, 0, 0, 0, 400, 400); if (!$image_copy) { throw new Exception( pht('Failed to save builtin avatar image data (imagecopy).')); } return PhabricatorImageTransformer::saveImageDataInAnyFormat( $canvas, 'image/png'); } private static function rgba2gd($rgba) { $r = $rgba[0]; $g = $rgba[1]; $b = $rgba[2]; $a = $rgba[3]; $a = (1 - $a) * 255; return ($a << 24) | ($r << 16) | ($g << 8) | $b; } public static function getImageMap() { $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/alphanumeric/'; $map = array(); $list = id(new FileFinder($root)) ->withType('f') ->withFollowSymlinks(true) ->find(); foreach ($list as $file) { $map['alphanumeric/'.$file] = $root.$file; } + return $map; } public function getUniqueProfileImage($username) { $pack_map = $this->getImagePackMap(); $image_map = $this->getImageMap(); $color_map = $this->getColorMap(); $border_map = $this->getBorderMap(); $file = phutil_utf8_strtoupper(substr($username, 0, 1)); $pack_count = count($pack_map); $color_count = count($color_map); $border_count = count($border_map); $pack_seed = $username.'_pack'; $color_seed = $username.'_color'; $border_seed = $username.'_border'; $pack_key = - PhabricatorHash::digestToRange($pack_seed, 1, $pack_count); + PhabricatorHash::digestToRange($pack_seed, 0, $pack_count - 1); $color_key = - PhabricatorHash::digestToRange($color_seed, 1, $color_count); + PhabricatorHash::digestToRange($color_seed, 0, $color_count - 1); $border_key = - PhabricatorHash::digestToRange($border_seed, 1, $border_count); + PhabricatorHash::digestToRange($border_seed, 0, $border_count - 1); $pack = $pack_map[$pack_key]; $icon = 'alphanumeric/'.$pack.'/'.$file.'.png'; $color = $color_map[$color_key]; $border = $border_map[$border_key]; if (!isset($image_map[$icon])) { $icon = 'alphanumeric/'.$pack.'/_default.png'; } return array('color' => $color, 'icon' => $icon, 'border' => $border); } public function getUserProfileImageFile($username) { $unique = $this->getUniqueProfileImage($username); $composer = id(new self()) ->setIcon($unique['icon']) ->setColor($unique['color']) ->setBorder($unique['border']); $data = $composer->loadBuiltinFileData(); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData( $data, array( 'name' => $composer->getBuiltinDisplayName(), 'profile' => true, 'canCDN' => true, )); unset($unguarded); return $file; } public static function getImagePackMap() { $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/alphanumeric/'; $map = id(new FileFinder($root)) ->withType('d') ->withFollowSymlinks(false) ->find(); - return $map; + return array_values($map); } public static function getBorderMap() { $map = array( array(0, 0, 0, 0), array(0, 0, 0, 0.3), array(255, 255, 255, 0.4), array(255, 255, 255, 0.7), ); return $map; } public static function getColorMap() { // // Generated Colors // http://tools.medialab.sciences-po.fr/iwanthue/ // $map = array( '#335862', '#2d5192', '#3c5da0', '#99cd86', '#704889', '#5ac59e', '#984060', '#33d4d1', '#9c4050', '#20d8fd', '#944937', '#4bd0e3', '#a25542', '#4eb4f3', '#6da8ec', '#545608', '#829ce5', '#68681d', '#607bc2', '#4b69ad', '#236ead', '#31a0de', '#4f8ed0', '#846f2a', '#bdb0f0', '#518342', '#9166aa', '#5e904e', '#f79dcc', '#158e6b', '#e189b7', '#3ba984', '#a85582', '#4cccb7', '#863d67', '#84c08c', '#7f4c7f', '#a1bb7a', '#65558f', '#445082', '#c9ca8e', '#265582', '#f4b189', '#265582', '#40b8e1', '#814a28', '#80c8f6', '#cf7b5d', '#1db5c7', '#c0606e', '#299a89', '#ef8ead', '#296437', '#d39edb', '#507436', '#b888c9', '#476025', '#9987c5', '#7867a3', '#769b5a', '#c46e9d', '#437d4e', '#d17492', '#115e41', '#ec8794', '#297153', '#d67381', '#57c2c3', '#bc607f', '#86ceac', '#7e3e53', '#72c8b8', '#884349', '#45a998', '#faa38c', '#265582', '#265582', '#e4b788', '#265582', '#bbbc81', '#265582', '#ccb781', '#265582', '#eb957f', '#15729c', '#cf996f', '#369bc5', '#b6685d', '#2da0a1', '#d38275', '#217e70', '#ec9da1', '#146268', '#e8aa95', '#3c6796', '#8da667', '#935f93', '#69a573', '#ae78ad', '#569160', '#d898be', '#8eb4e8', '#5e622c', '#929ad3', '#6c8548', '#576196', '#aed0a0', '#694e79', '#9abb8d', '#8c5175', '#6bb391', '#8b4a5f', '#519878', '#ae7196', '#3d8465', '#e69eb3', '#48663d', '#cdaede', '#71743d', '#63acda', '#7b5d30', '#66bed6', '#3585b0', '#5880b0', '#739acc', '#48a3ba', '#9d565b', '#7fc4ca', '#99566b', '#94cabf', '#7b4b49', '#b1c8eb', '#4e5632', '#ecb2c3', '#2d6158', '#cf8287', '#25889f', '#b2696d', '#6bafb6', '#8c5744', '#84b9d6', '#9db3d6', '#777cad', '#826693', '#86a779', '#9d7fad', '#b193c2', '#547348', '#d5adcb', '#3f674d', '#c98398', '#66865a', '#b2add6', '#5a623d', '#9793bb', '#3c5472', '#d5c5a1', '#5e5a7f', '#2c647e', '#d8b194', '#49607f', '#c7b794', '#335862', '#e3aba7', '#335862', '#d9b9ad', '#335862', '#c48975', '#347b81', '#ad697e', '#799a6d', '#916b88', '#69536b', '#b4c4ad', '#845865', '#96b89d', '#706d92', '#9aa27a', '#5b7292', '#bc967b', '#417792', '#ce9793', '#335862', '#c898a5', '#527a5f', '#b38ba9', '#648d72', '#986b78', '#79afa4', '#966461', '#50959b', '#b27d7a', '#335862', '#335862', '#bcadc4', '#706343', '#749ebc', '#8c6a50', '#92b8c4', '#758cad', '#868e67', '#335862', '#335862', '#335862', '#ac7e8b', '#77a185', '#807288', '#636f51', '#a192a9', '#467a70', '#9b7d73', '#335862', '#335862', '#8c9c85', '#335862', '#81645a', '#5f9489', '#335862', '#789da8', '#335862', '#72826c', '#335862', '#5c8596', '#335862', '#456a74', '#335862', '#335862', '#335862', ); return $map; } } diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index 7b5780460b..05b5fa719d 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -1,142 +1,143 @@ openEnvelope(); if (!$result) { throw new Exception(pht('Trying to digest empty password!')); } for ($ii = 0; $ii < 1000; $ii++) { $result = self::digest($result, $salt); } return $result; } /** * 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 */ 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; } 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); - $value = head(unpack('L', $hash)); + // Make sure this ends up positive, even on 32-bit machines. + $value = head(unpack('L', $hash)) & 0x7FFFFFFF; - return $min + ($value % ($max - $min)); + 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; } }