Skip to content

Commit ae157d1

Browse files
committed
Added new Utilities
1 parent 847018f commit ae157d1

File tree

8 files changed

+302
-2
lines changed

8 files changed

+302
-2
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
.idea
2+
vendor
3+
composer.lock

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
}
2222
},
2323
"require": {
24-
"php": "^7.2",
24+
"php": "^7.4",
2525
"laravel/framework": "^7",
26-
"intervention/image": "^2.5"
26+
"intervention/image": "^2.5",
27+
"nesbot/carbon": "^2.36",
28+
"ext-json": "*"
2729
}
2830
}

src/Image/ImageRepresentation.php

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace le0daniel\Laravel\ImageEngine\Image;
4+
5+
use Carbon\Carbon;
6+
use le0daniel\Laravel\ImageEngine\Utility\Arrays;
7+
use le0daniel\Laravel\ImageEngine\Utility\Base64;
8+
use le0daniel\Laravel\ImageEngine\Utility\Json;
9+
10+
/**
11+
* Class ImageRepresentation
12+
* @package le0daniel\Laravel\ImageEngine\Image
13+
*
14+
* @property-read string $filePath
15+
* @property-read string $size
16+
* @property-read null|int $expires
17+
* @property-read null|Carbon $expiresCarbon
18+
* @property-read string $diskName
19+
* @property-read int|null $timestamp
20+
* @property-read string $extension
21+
* @property-read bool $isConfidential
22+
* @property-read bool $isExpired
23+
*/
24+
final class ImageRepresentation
25+
{
26+
private const DEFAULT_DISK_NAME = 'local';
27+
private const SERIALIZE_KEY_MAP = [
28+
'filePath' => 'p',
29+
'size' => 's',
30+
'expires' => 'e',
31+
'diskName' => 'd',
32+
];
33+
private const IMAGE_CACHE_MAX_AGE = 31557600;
34+
private array $attributes;
35+
36+
private function __construct(array $attributes)
37+
{
38+
$this->attributes = $attributes;
39+
}
40+
41+
/**
42+
* @param string $filePath
43+
* @param string $size
44+
* @param null|Carbon|int $expires
45+
* @param string $diskName
46+
* @return static
47+
*/
48+
public static function from(
49+
string $filePath,
50+
string $size,
51+
$expires = null,
52+
string $diskName = self::DEFAULT_DISK_NAME
53+
): self {
54+
return new self(
55+
[
56+
'filePath' => $filePath,
57+
'size' => $size,
58+
'expires' => $expires instanceof Carbon
59+
? $expires->getTimestamp()
60+
: $expires,
61+
'diskName' => $diskName === self::DEFAULT_DISK_NAME
62+
? null
63+
: $diskName
64+
,
65+
]
66+
);
67+
}
68+
69+
public static function fromSerialized(string $serialized): self
70+
{
71+
$mappedAttributes = Json::decode(Base64::urlDecode($serialized), ['e' => null, 'd' => null]);
72+
$attributes = Arrays::mapKeys(
73+
$mappedAttributes,
74+
array_flip(self::SERIALIZE_KEY_MAP)
75+
);
76+
return new self($attributes);
77+
}
78+
79+
public function __set($name, $value)
80+
{
81+
throw new \RuntimeException("Can not modify immutable ImageRepresentation. Tried to modify `{$name}`");
82+
}
83+
84+
public function __isset(string $name)
85+
{
86+
return array_key_exists($name, $this->attributes);
87+
}
88+
89+
private function getValue(string $name, $value)
90+
{
91+
switch ($name) {
92+
case 'timestamp':
93+
return $this->expires;
94+
case 'disk':
95+
return $value ?? self::DEFAULT_DISK_NAME;
96+
case 'extension':
97+
return pathinfo($this->filePath, PATHINFO_EXTENSION);
98+
case 'isConfidential':
99+
return (bool)$this->expires;
100+
case 'isExpired':
101+
return $this->isConfidential ? $this->expiresCarbon->isPast() : false;
102+
}
103+
104+
return $value;
105+
}
106+
107+
private function timeToExpireInSeconds(): int
108+
{
109+
return $this->isConfidential
110+
? $this->expires - time()
111+
: self::IMAGE_CACHE_MAX_AGE;
112+
}
113+
114+
public function cacheControlHeaders(): array
115+
{
116+
$maxAgeInSeconds = min($this->timeToExpireInSeconds(), self::IMAGE_CACHE_MAX_AGE);
117+
118+
return [
119+
'Cache-Control' => "max-age={$maxAgeInSeconds}, public",
120+
];
121+
}
122+
123+
public function __get(string $name)
124+
{
125+
return $this->__isset($name)
126+
? $this->getValue($name, $this->attributes[$name])
127+
: $this->getValue($name, null);
128+
}
129+
130+
public function toArray()
131+
{
132+
$array = Arrays::removeNullValues(
133+
Arrays::mapKeys($this->attributes, self::SERIALIZE_KEY_MAP)
134+
);
135+
asort($array);
136+
return $array;
137+
}
138+
139+
public function serialize(): string
140+
{
141+
return Base64::urlEncode(
142+
Json::encode($this->toArray())
143+
);
144+
}
145+
146+
public function dump()
147+
{
148+
var_dump($this->attributes);
149+
}
150+
}

src/Utility/Arrays.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace le0daniel\Laravel\ImageEngine\Utility;
6+
7+
final class Arrays
8+
{
9+
public static function mapKeys(array $array, array $keyMap): array
10+
{
11+
$mappedArray = [];
12+
13+
foreach ($array as $key => $value) {
14+
$mappedKey = array_key_exists($key, $keyMap)
15+
? $keyMap[$key]
16+
: $key;
17+
$mappedArray[$mappedKey] = $value;
18+
}
19+
20+
return $mappedArray;
21+
}
22+
23+
public static function removeNullValues(array $array): array
24+
{
25+
return array_filter($array, fn($value) => !is_null($value));
26+
}
27+
28+
public static function applyDefaultValues(array $array, array $defaultValues): array
29+
{
30+
foreach ($defaultValues as $key => $defaultValue) {
31+
if (!array_key_exists($key, $array)) {
32+
$array[$key] = $defaultValue;
33+
}
34+
}
35+
return $array;
36+
}
37+
38+
}

src/Utility/Base64.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace le0daniel\Laravel\ImageEngine\Utility;
6+
7+
final class Base64
8+
{
9+
10+
public static function encode(string $string): string
11+
{
12+
return base64_encode($string);
13+
}
14+
15+
public static function decode(string $encodedString): string
16+
{
17+
return base64_decode($encodedString);
18+
}
19+
20+
public static function urlEncode(string $string): string
21+
{
22+
$urlSafe = strtr(self::encode($string), '+/', '-_');
23+
return rtrim($urlSafe, '=');
24+
}
25+
26+
public static function urlDecode(string $urlEncodedString): string
27+
{
28+
return self::decode(
29+
strtr($urlEncodedString, '-_', '+/')
30+
);
31+
}
32+
33+
}

src/Utility/Json.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace le0daniel\Laravel\ImageEngine\Utility;
6+
7+
final class Json
8+
{
9+
10+
public static function encode(array $data): string
11+
{
12+
return json_encode($data, JSON_THROW_ON_ERROR);
13+
}
14+
15+
public static function decode(string $data, ?array $defaultValues): array
16+
{
17+
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
18+
return $defaultValues
19+
? Arrays::applyDefaultValues($data, $defaultValues)
20+
: $data;
21+
}
22+
23+
}

src/Utility/SignatureException.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace le0daniel\Laravel\ImageEngine\Utility;
4+
5+
final class SignatureException extends \Exception
6+
{
7+
8+
}

src/Utility/Signatures.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace le0daniel\Laravel\ImageEngine\Utility;
4+
5+
final class Signatures
6+
{
7+
private const SIGNATURE_REGEX = '/^[^:]*::[a-zA-Z0-9\-\_]+$/';
8+
9+
public static function sign(string $secret, string $stringToSign): string
10+
{
11+
$signature = self::calculateSignature($secret, $stringToSign);
12+
return $stringToSign . '::' . Base64::urlEncode($signature);
13+
}
14+
15+
public static function verifyAndReturnPayloadString(string $secret, string $signedString): string
16+
{
17+
self::verifyStructure($signedString);
18+
[$payload, $userProvidedSignature] = explode('::', $signedString, 2);
19+
self::verifySignature($secret, $payload, $userProvidedSignature);
20+
return $payload;
21+
}
22+
23+
private static function calculateSignature(string $secret, string $stringToSign): string
24+
{
25+
return hash_hmac('sha256', $stringToSign, $secret, true);
26+
}
27+
28+
private static function verifyStructure(string $signedString): void
29+
{
30+
if (!preg_match(self::SIGNATURE_REGEX, $signedString, $matches, PREG_OFFSET_CAPTURE, 0)) {
31+
throw new SignatureException('The signed string is of invalid structure');
32+
}
33+
}
34+
35+
private static function verifySignature(string $secret, $payload, $userProvidedSignature): void
36+
{
37+
$signature = self::calculateSignature($secret, $payload);
38+
39+
if (!hash_equals($signature, Base64::urlDecode($userProvidedSignature))) {
40+
throw new SignatureException('Signature mismatches.');
41+
}
42+
}
43+
44+
}

0 commit comments

Comments
 (0)