ExpiringToken::getExpiresOn()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Pageon\ExpiringToken;
4
5
use DateInterval;
6
use DateTimeImmutable;
7
use Throwable;
8
9
/**
10
 * A random token of 128 characters with an expiration date.
11
 *
12
 * The token uses base64 but the length is calculated so that it is url safe, so no padding with the = character.
13
 */
14
final class ExpiringToken
15
{
16
    /** @var string */
17
    const DEFAULT_DATE_INTERVAL = 'P3D';
18
19
    /** @var DateTimeImmutable */
20
    private $expiresOn;
21
22
    /** @var string */
23
    private $token;
24
25
    /**
26
     * @param DateTimeImmutable $expiresOn
27
     * @param string $token
28
     */
29 7
    private function __construct(DateTimeImmutable $expiresOn, $token)
30
    {
31 7
        $this->expiresOn = $expiresOn;
32 7
        $this->token = $token;
33 7
    }
34
35
    /**
36
     * @param DateInterval|null $dateInterval If the interval isn't specified the token will expire in 3 days
37
     *
38
     * @return self
39
     */
40 7
    public static function create(DateInterval $dateInterval = null): self
41
    {
42 7
        return new self(
43 7
            (new DateTimeImmutable())->add(
44 7
                $dateInterval ?? new DateInterval(self::DEFAULT_DATE_INTERVAL)
45
            ),
46 7
            bin2hex(random_bytes(32))
47
        );
48
    }
49
50
    /**
51
     * @param string $string The string representation of a random token created via the __toString method.
52
     *
53
     * @throws InvalidToken
54
     *
55
     * @return ExpiringToken
56
     */
57 5
    public static function fromString(string $string): self
58
    {
59 5
        $decodedString = base64_decode($string);
60
61 5
        if (!$decodedString) {
62
            // couldn't decode the token
63 1
            throw InvalidToken::unableToDecode();
64
        }
65
66 4
        $tokenParts = explode('_token:', $decodedString, 2);
67
68 4
        if (count($tokenParts) !== 2) {
69 2
            throw InvalidToken::unableToDecode();
70
        }
71
72
        try {
73 2
            return new self(new DateTimeImmutable($tokenParts[0]), $tokenParts[1]);
74 1
        } catch (Throwable $throwable) {
75 1
            throw InvalidToken::unableToDecode();
76
        }
77
    }
78
79
    /**
80
     * @return string
81
     */
82 2
    public function __toString(): string
83
    {
84 2
        return base64_encode($this->expiresOn->format(DATE_ATOM) . '_token:' . $this->token);
85
    }
86
87
    /**
88
     * @param ExpiringToken $expiringToken
89
     *
90
     * @throws InvalidToken
91
     * @throws TokenHasExpired
92
     *
93
     * @return bool
94
     */
95 3
    public function validateAgainst(ExpiringToken $expiringToken): bool
96
    {
97 3
        if (!$this->equals($expiringToken)) {
98 1
            throw InvalidToken::tokensDoNotMatch();
99
        }
100
101 2
        if ($this->hasExpired()) {
102 1
            throw new TokenHasExpired();
103
        }
104
105 1
        return true;
106
    }
107
108
    /**
109
     * @return bool
110
     */
111 4
    public function hasExpired(): bool
112
    {
113 4
        return new DateTimeImmutable() > $this->expiresOn;
114
    }
115
116
    /**
117
     * @return DateTimeImmutable
118
     */
119 2
    public function getExpiresOn(): DateTimeImmutable
120
    {
121 2
        return $this->expiresOn;
122
    }
123
124
    /**
125
     * @param ExpiringToken $expiringToken
126
     *
127
     * @return bool
128
     */
129 3
    private function equals(ExpiringToken $expiringToken): bool
130
    {
131 3
        return $this->token === $expiringToken->token && $this->expiresOn == $expiringToken->expiresOn;
132
    }
133
}
134