Avatar.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. use Behat\Gherkin\Node\TableNode;
  7. use PHPUnit\Framework\Assert;
  8. require __DIR__ . '/../../vendor/autoload.php';
  9. trait Avatar {
  10. /** @var string * */
  11. private $lastAvatar;
  12. /** @AfterScenario **/
  13. public function cleanupLastAvatar() {
  14. $this->lastAvatar = null;
  15. }
  16. private function getLastAvatar() {
  17. $this->lastAvatar = '';
  18. $body = $this->response->getBody();
  19. while (!$body->eof()) {
  20. $this->lastAvatar .= $body->read(8192);
  21. }
  22. $body->close();
  23. }
  24. /**
  25. * @When user :user gets avatar for user :userAvatar
  26. *
  27. * @param string $user
  28. * @param string $userAvatar
  29. */
  30. public function userGetsAvatarForUser(string $user, string $userAvatar) {
  31. $this->userGetsAvatarForUserWithSize($user, $userAvatar, '128');
  32. }
  33. /**
  34. * @When user :user gets avatar for user :userAvatar with size :size
  35. *
  36. * @param string $user
  37. * @param string $userAvatar
  38. * @param string $size
  39. */
  40. public function userGetsAvatarForUserWithSize(string $user, string $userAvatar, string $size) {
  41. $this->asAn($user);
  42. $this->sendingToDirectUrl('GET', '/index.php/avatar/' . $userAvatar . '/' . $size);
  43. $this->theHTTPStatusCodeShouldBe('200');
  44. $this->getLastAvatar();
  45. }
  46. /**
  47. * @When user :user gets avatar for guest :guestAvatar
  48. *
  49. * @param string $user
  50. * @param string $guestAvatar
  51. */
  52. public function userGetsAvatarForGuest(string $user, string $guestAvatar) {
  53. $this->asAn($user);
  54. $this->sendingToDirectUrl('GET', '/index.php/avatar/guest/' . $guestAvatar . '/128');
  55. $this->theHTTPStatusCodeShouldBe('201');
  56. $this->getLastAvatar();
  57. }
  58. /**
  59. * @When logged in user gets temporary avatar
  60. */
  61. public function loggedInUserGetsTemporaryAvatar() {
  62. $this->loggedInUserGetsTemporaryAvatarWith('200');
  63. }
  64. /**
  65. * @When logged in user gets temporary avatar with :statusCode
  66. *
  67. * @param string $statusCode
  68. */
  69. public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) {
  70. $this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp');
  71. $this->theHTTPStatusCodeShouldBe($statusCode);
  72. $this->getLastAvatar();
  73. }
  74. /**
  75. * @When logged in user posts temporary avatar from file :source
  76. *
  77. * @param string $source
  78. */
  79. public function loggedInUserPostsTemporaryAvatarFromFile(string $source) {
  80. $file = \GuzzleHttp\Psr7\Utils::streamFor(fopen($source, 'r'));
  81. $this->sendingAToWithRequesttoken('POST', '/index.php/avatar',
  82. [
  83. 'multipart' => [
  84. [
  85. 'name' => 'files[]',
  86. 'contents' => $file
  87. ]
  88. ]
  89. ]);
  90. $this->theHTTPStatusCodeShouldBe('200');
  91. }
  92. /**
  93. * @When logged in user posts temporary avatar from internal path :path
  94. *
  95. * @param string $path
  96. */
  97. public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) {
  98. $this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path);
  99. $this->theHTTPStatusCodeShouldBe('200');
  100. }
  101. /**
  102. * @When logged in user crops temporary avatar
  103. *
  104. * @param TableNode $crop
  105. */
  106. public function loggedInUserCropsTemporaryAvatar(TableNode $crop) {
  107. $this->loggedInUserCropsTemporaryAvatarWith('200', $crop);
  108. }
  109. /**
  110. * @When logged in user crops temporary avatar with :statusCode
  111. *
  112. * @param string $statusCode
  113. * @param TableNode $crop
  114. */
  115. public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) {
  116. $parameters = [];
  117. foreach ($crop->getRowsHash() as $key => $value) {
  118. $parameters[] = 'crop[' . $key . ']=' . $value;
  119. }
  120. $this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters));
  121. $this->theHTTPStatusCodeShouldBe($statusCode);
  122. }
  123. /**
  124. * @When logged in user deletes the user avatar
  125. */
  126. public function loggedInUserDeletesTheUserAvatar() {
  127. $this->sendingAToWithRequesttoken('DELETE', '/index.php/avatar');
  128. $this->theHTTPStatusCodeShouldBe('200');
  129. }
  130. /**
  131. * @Then last avatar is a square of size :size
  132. *
  133. * @param string size
  134. */
  135. public function lastAvatarIsASquareOfSize(string $size) {
  136. [$width, $height] = getimagesizefromstring($this->lastAvatar);
  137. Assert::assertEquals($width, $height, 'Expected avatar to be a square');
  138. Assert::assertEquals($size, $width);
  139. }
  140. /**
  141. * @Then last avatar is not a square
  142. */
  143. public function lastAvatarIsNotASquare() {
  144. [$width, $height] = getimagesizefromstring($this->lastAvatar);
  145. Assert::assertNotEquals($width, $height, 'Expected avatar to not be a square');
  146. }
  147. /**
  148. * @Then last avatar is not a single color
  149. */
  150. public function lastAvatarIsNotASingleColor() {
  151. Assert::assertEquals(null, $this->getColorFromLastAvatar());
  152. }
  153. /**
  154. * @Then last avatar is a single :color color
  155. *
  156. * @param string $color
  157. * @param string $size
  158. */
  159. public function lastAvatarIsASingleColor(string $color) {
  160. $expectedColor = $this->hexStringToRgbColor($color);
  161. $colorFromLastAvatar = $this->getColorFromLastAvatar();
  162. Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar),
  163. $this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color);
  164. }
  165. private function hexStringToRgbColor($hexString) {
  166. // Strip initial "#"
  167. $hexString = substr($hexString, 1);
  168. $rgbColorInt = hexdec($hexString);
  169. // RGBA hex strings are not supported; the given string is assumed to be
  170. // an RGB hex string.
  171. return [
  172. 'red' => ($rgbColorInt >> 16) & 0xFF,
  173. 'green' => ($rgbColorInt >> 8) & 0xFF,
  174. 'blue' => $rgbColorInt & 0xFF,
  175. 'alpha' => 0
  176. ];
  177. }
  178. private function rgbColorToHexString($rgbColor) {
  179. $rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']);
  180. return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT);
  181. }
  182. private function getColorFromLastAvatar() {
  183. $image = imagecreatefromstring($this->lastAvatar);
  184. $firstPixelColorIndex = imagecolorat($image, 0, 0);
  185. $firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex);
  186. for ($i = 0; $i < imagesx($image); $i++) {
  187. for ($j = 0; $j < imagesx($image); $j++) {
  188. $currentPixelColorIndex = imagecolorat($image, $i, $j);
  189. $currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex);
  190. // The colors are compared with a small allowed delta, as even
  191. // on solid color images the resizing can cause some small
  192. // artifacts that slightly modify the color of certain pixels.
  193. if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) {
  194. imagedestroy($image);
  195. return null;
  196. }
  197. }
  198. }
  199. imagedestroy($image);
  200. return $firstPixelColor;
  201. }
  202. private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) {
  203. if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) &&
  204. $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) &&
  205. $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) &&
  206. $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) {
  207. return true;
  208. }
  209. return false;
  210. }
  211. private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) {
  212. if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) &&
  213. $firstColorComponent <= ($secondColorComponent + $allowedDelta)) {
  214. return true;
  215. }
  216. return false;
  217. }
  218. }