-
Notifications
You must be signed in to change notification settings - Fork 135
/
Copy pathKeyGeneratorService.php
145 lines (122 loc) · 4.28 KB
/
KeyGeneratorService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php
namespace App\Services;
use App\Models\Url;
use Illuminate\Support\Str;
class KeyGeneratorService
{
private const HASH_CHAR = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
/**
* Generate a short string that can be used as a unique key for the shortened
* url.
*
* @return string A unique string to use as the shortened url key
*/
public function generate(string $value): string
{
$key = $this->generateSimpleString($value);
if (
$this->assertStringCanBeUsedAsKey($key) === false
|| strlen($key) < config('urlhub.hash_length')
) {
$key = $this->generateRandomString();
}
return $key;
}
public function generateSimpleString(string $value): string
{
return Str::of($value)
// Remove all characters except `0-9a-z-AZ`
->replaceMatches('/[^'.self::HASH_CHAR.']/i', '')
// Take the specified number of characters from the end of the string.
->substr(config('urlhub.hash_length') * -1)
->lower();
}
/**
* Generate a random string of specified length. The string will only contain
* characters from the specified character set.
*
* @return string The generated random string
*/
public function generateRandomString(): string
{
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
do {
$urlKey = $generator->generateString(config('urlhub.hash_length'), self::HASH_CHAR);
} while ($this->assertStringCanBeUsedAsKey($urlKey) == false);
return $urlKey;
}
/**
* Check if string can be used as a keyword.
*
* This function will check under several conditions:
* 1. If the string is already used as a key
* 2. If the string is in the list of reserved keywords
* 3. If the string is in the route path list
*
* If any or all of the above conditions are met, then the string cannot be
* used as a keyword and must return false.
*/
public function assertStringCanBeUsedAsKey(string $value): bool
{
$route = array_map(fn (\Illuminate\Routing\Route $route) => $route->uri,
\Illuminate\Support\Facades\Route::getRoutes()->get()
);
$alreadyInUse = Url::whereKeyword($value)->exists();
$isReservedKeyword = in_array($value, config('urlhub.reserved_keyword'));
$isRoute = in_array($value, $route);
if ($alreadyInUse || $isReservedKeyword || $isRoute) {
return false;
}
return true;
}
/*
|--------------------------------------------------------------------------
| Capacity calculation
|--------------------------------------------------------------------------
*/
/**
* The maximum number of unique strings that can be generated.
*/
public function possibleOutput(): int
{
$nChar = strlen(self::HASH_CHAR);
$strLen= config('urlhub.hash_length');
// for testing purposes only
// tests\Unit\Middleware\UrlHubLinkCheckerTest.php
if ($strLen === 0) {
return 0;
}
return gmp_intval(gmp_pow($nChar, $strLen));
}
/**
* The number of unique keywords that have been used.
*
* Formula:
* totalKey = randomKey + customKey
*
* The length of the generated string (randomKey) and the length of the
* `customKey` string must be identical.
*/
public function totalKey(): int
{
$hashLength = (int) config('urlhub.hash_length');
$regexPattern = '['.self::HASH_CHAR.']{'.$hashLength.'}';
$randomKey = Url::whereIsCustom(false)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->count();
$customKey = Url::whereIsCustom(true)
->whereRaw('LENGTH(keyword) = ?', [$hashLength])
->whereRaw("keyword REGEXP '".$regexPattern."'")
->count();
return $randomKey + $customKey;
}
/**
* Calculate the number of unique random strings that can still be generated.
*/
public function remainingCapacity(): int
{
// prevent negative values
return max($this->possibleOutput() - $this->totalKey(), 0);
}
}