1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Retour plugin for Craft CMS 3.x |
4
|
|
|
* |
5
|
|
|
* Retour allows you to intelligently redirect legacy URLs, so that you don't |
6
|
|
|
* lose SEO value when rebuilding & restructuring a website |
7
|
|
|
* |
8
|
|
|
* @link https://nystudio107.com/ |
9
|
|
|
* @copyright Copyright (c) 2018 nystudio107 |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace nystudio107\retour\services; |
13
|
|
|
|
14
|
|
|
use nystudio107\retour\Retour; |
15
|
|
|
use nystudio107\retour\events\RedirectEvent; |
16
|
|
|
use nystudio107\retour\events\ResolveRedirectEvent; |
17
|
|
|
use nystudio107\retour\models\StaticRedirects as StaticRedirectsModel; |
18
|
|
|
|
19
|
|
|
use Craft; |
20
|
|
|
use craft\base\Component; |
21
|
|
|
use craft\base\Plugin; |
22
|
|
|
use craft\db\Query; |
23
|
|
|
use craft\errors\SiteNotFoundException; |
24
|
|
|
use craft\helpers\Db; |
25
|
|
|
use craft\helpers\UrlHelper; |
26
|
|
|
|
27
|
|
|
use yii\base\ExitException; |
28
|
|
|
use yii\base\InvalidConfigException; |
29
|
|
|
use yii\base\InvalidRouteException; |
30
|
|
|
use yii\caching\TagDependency; |
31
|
|
|
use yii\db\Exception; |
32
|
|
|
|
33
|
|
|
/** @noinspection MissingPropertyAnnotationsInspection */ |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @author nystudio107 |
37
|
|
|
* @package Retour |
38
|
|
|
* @since 3.0.0 |
39
|
|
|
*/ |
40
|
|
|
class Redirects extends Component |
41
|
|
|
{ |
42
|
|
|
// Constants |
43
|
|
|
// ========================================================================= |
44
|
|
|
|
45
|
|
|
const CACHE_KEY = 'retour_redirect_'; |
46
|
|
|
|
47
|
|
|
const GLOBAL_REDIRECTS_CACHE_TAG = 'retour_redirects'; |
48
|
|
|
|
49
|
|
|
const EVENT_REDIRECT_ID = 0; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @event RedirectEvent The event that is triggered before the redirect is saved |
53
|
|
|
* You may set [[RedirectEvent::isValid]] to `false` to prevent the redirect from getting saved. |
54
|
|
|
* |
55
|
|
|
* ```php |
56
|
|
|
* use nystudio107\retour\services\Redirects; |
57
|
|
|
* use nystudio107\retour\events\RedirectEvent; |
58
|
|
|
* |
59
|
|
|
* Event::on(Redirects::class, |
60
|
|
|
* Redirects::EVENT_BEFORE_SAVE_REDIRECT, |
61
|
|
|
* function(RedirectEvent $event) { |
62
|
|
|
* // potentially set $event->isValid; |
63
|
|
|
* } |
64
|
|
|
* ); |
65
|
|
|
* ``` |
66
|
|
|
*/ |
67
|
|
|
const EVENT_BEFORE_SAVE_REDIRECT = 'beforeSaveRedirect'; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @event RedirectEvent The event that is triggered after the redirect is saved |
71
|
|
|
* |
72
|
|
|
* ```php |
73
|
|
|
* use nystudio107\retour\services\Redirects; |
74
|
|
|
* use nystudio107\retour\events\RedirectEvent; |
75
|
|
|
* |
76
|
|
|
* Event::on(Redirects::class, |
77
|
|
|
* Redirects::EVENT_AFTER_SAVE_REDIRECT, |
78
|
|
|
* function(RedirectEvent $event) { |
79
|
|
|
* // the redirect was saved |
80
|
|
|
* } |
81
|
|
|
* ); |
82
|
|
|
* ``` |
83
|
|
|
*/ |
84
|
|
|
const EVENT_AFTER_SAVE_REDIRECT = 'afterSaveRedirect'; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @event ResolveRedirectEvent The event that is triggered before Retour has attempted |
88
|
|
|
* to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to |
89
|
|
|
* to the URL that it should redirect to, or null if no redirect should happen |
90
|
|
|
* |
91
|
|
|
* ```php |
92
|
|
|
* use nystudio107\retour\services\Redirects; |
93
|
|
|
* use nystudio107\retour\events\ResolveRedirectEvent; |
94
|
|
|
* |
95
|
|
|
* Event::on(Redirects::class, |
96
|
|
|
* Redirects::EVENT_AFTER_SAVE_REDIRECT, |
97
|
|
|
* function(ResolveRedirectEvent $event) { |
98
|
|
|
* // potentially set $event->redirectDestUrl; |
99
|
|
|
* } |
100
|
|
|
* ); |
101
|
|
|
* ``` |
102
|
|
|
*/ |
103
|
|
|
const EVENT_BEFORE_RESOLVE_REDIRECT = 'beforeResolveRedirect'; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @event ResolveRedirectEvent The event that is triggered after Retour has attempted |
107
|
|
|
* to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to |
108
|
|
|
* to the URL that it should redirect to, or null if no redirect should happen |
109
|
|
|
* |
110
|
|
|
* ```php |
111
|
|
|
* use nystudio107\retour\services\Redirects; |
112
|
|
|
* use nystudio107\retour\events\ResolveRedirectEvent; |
113
|
|
|
* |
114
|
|
|
* Event::on(Redirects::class, |
115
|
|
|
* Redirects::EVENT_AFTER_RESOLVE_REDIRECT, |
116
|
|
|
* function(ResolveRedirectEvent $event) { |
117
|
|
|
* // potentially set $event->redirectDestUrl; |
118
|
|
|
* } |
119
|
|
|
* ); |
120
|
|
|
* ``` |
121
|
|
|
*/ |
122
|
|
|
const EVENT_AFTER_RESOLVE_REDIRECT = 'afterResolveRedirect'; |
123
|
|
|
|
124
|
|
|
// Protected Properties |
125
|
|
|
// ========================================================================= |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @var null|array |
129
|
|
|
*/ |
130
|
|
|
protected $cachedStaticRedirects; |
131
|
|
|
|
132
|
|
|
// Public Methods |
133
|
|
|
// ========================================================================= |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Handle 404s by looking for redirects |
137
|
|
|
*/ |
138
|
|
|
public function handle404() |
139
|
|
|
{ |
140
|
|
|
Craft::info( |
141
|
|
|
Craft::t( |
142
|
|
|
'retour', |
143
|
|
|
'A 404 exception occurred' |
144
|
|
|
), |
145
|
|
|
__METHOD__ |
146
|
|
|
); |
147
|
|
|
$request = Craft::$app->getRequest(); |
148
|
|
|
// We only want site requests that are not live preview or console requests |
149
|
|
|
if ($request->getIsSiteRequest() && !$request->getIsLivePreview() && !$request->getIsConsoleRequest()) { |
|
|
|
|
150
|
|
|
// See if we should redirect |
151
|
|
|
try { |
152
|
|
|
$fullUrl = urldecode($request->getAbsoluteUrl()); |
153
|
|
|
$pathOnly = urldecode($request->getUrl()); |
154
|
|
|
} catch (InvalidConfigException $e) { |
155
|
|
|
Craft::error( |
156
|
|
|
$e->getMessage(), |
157
|
|
|
__METHOD__ |
158
|
|
|
); |
159
|
|
|
$pathOnly = ''; |
160
|
|
|
$fullUrl = ''; |
161
|
|
|
} |
162
|
|
|
// Strip the query string if `alwaysStripQueryString` is set |
163
|
|
|
if (Retour::$settings->alwaysStripQueryString) { |
164
|
|
|
$fullUrl = UrlHelper::stripQueryString($fullUrl); |
165
|
|
|
$pathOnly = UrlHelper::stripQueryString($pathOnly); |
166
|
|
|
} |
167
|
|
|
Craft::info( |
168
|
|
|
Craft::t( |
169
|
|
|
'retour', |
170
|
|
|
'404 full URL: {fullUrl}, 404 path only: {pathOnly}', |
171
|
|
|
['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly] |
172
|
|
|
), |
173
|
|
|
__METHOD__ |
174
|
|
|
); |
175
|
|
|
if (!$this->excludeUri($pathOnly)) { |
176
|
|
|
// Redirect if we find a match, otherwise let Craft handle it |
177
|
|
|
$redirect = $this->findRedirectMatch($fullUrl, $pathOnly); |
178
|
|
|
if (!$this->doRedirect($fullUrl, $pathOnly, $redirect) && !Retour::$settings->alwaysStripQueryString) { |
179
|
|
|
// Try it again without the query string |
180
|
|
|
$fullUrl = UrlHelper::stripQueryString($fullUrl); |
181
|
|
|
$pathOnly = UrlHelper::stripQueryString($pathOnly); |
182
|
|
|
$redirect = $this->findRedirectMatch($fullUrl, $pathOnly); |
183
|
|
|
$this->doRedirect($fullUrl, $pathOnly, $redirect); |
184
|
|
|
} |
185
|
|
|
// Increment the stats |
186
|
|
|
Retour::$plugin->statistics->incrementStatistics($pathOnly, false); |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Do the redirect |
193
|
|
|
* |
194
|
|
|
* @param string $fullUrl |
195
|
|
|
* @param string $pathOnly |
196
|
|
|
* @param null|array $redirect |
197
|
|
|
* |
198
|
|
|
* @return bool false if not redirected |
199
|
|
|
*/ |
200
|
|
|
public function doRedirect(string $fullUrl, string $pathOnly, $redirect): bool |
201
|
|
|
{ |
202
|
|
|
$response = Craft::$app->getResponse(); |
203
|
|
|
if ($redirect !== null) { |
204
|
|
|
// Figure out what type of source matching was done |
205
|
|
|
$redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly'; |
206
|
|
|
switch ($redirectSrcMatch) { |
207
|
|
|
case 'pathonly': |
208
|
|
|
$url = $pathOnly; |
209
|
|
|
break; |
210
|
|
|
case 'fullurl': |
211
|
|
|
$url = $fullUrl; |
212
|
|
|
break; |
213
|
|
|
default: |
214
|
|
|
$url = $pathOnly; |
215
|
|
|
break; |
216
|
|
|
} |
217
|
|
|
$dest = $redirect['redirectDestUrl']; |
218
|
|
|
// If this isn't a full URL, make it one based on the appropriate site |
219
|
|
|
if (!UrlHelper::isFullUrl($dest)) { |
220
|
|
|
try { |
221
|
|
|
$dest = UrlHelper::siteUrl($dest, null, null, $redirect['siteId'] ?? null); |
222
|
|
|
} catch (\yii\base\Exception $e) { |
|
|
|
|
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
if (Retour::$settings->preserveQueryString) { |
226
|
|
|
$request = Craft::$app->getRequest(); |
227
|
|
|
if (!empty($request->getQueryStringWithoutPath())) { |
228
|
|
|
$dest .= '?' . $request->getQueryStringWithoutPath(); |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
$redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound'; |
232
|
|
|
// Parse reference tags for exact matches |
233
|
|
|
if ($redirectMatchType === 'exactmatch') { |
234
|
|
|
$dest = Craft::$app->elements->parseRefs($dest, $redirect['siteId'] ?? null); |
235
|
|
|
} |
236
|
|
|
$status = $redirect['redirectHttpCode']; |
237
|
|
|
Craft::info( |
238
|
|
|
Craft::t( |
239
|
|
|
'retour', |
240
|
|
|
'Redirecting {url} to {dest} with status {status}', |
241
|
|
|
['url' => $url, 'dest' => $dest, 'status' => $status] |
242
|
|
|
), |
243
|
|
|
__METHOD__ |
244
|
|
|
); |
245
|
|
|
// Increment the stats |
246
|
|
|
Retour::$plugin->statistics->incrementStatistics($url, true); |
247
|
|
|
// Handle a Retour return status > 400 to render the actual error template |
248
|
|
|
if ($status >= 400) { |
249
|
|
|
Retour::$currentException->statusCode = $status; |
250
|
|
|
$errorHandler = Craft::$app->getErrorHandler(); |
251
|
|
|
$errorHandler->exception = Retour::$currentException; |
252
|
|
|
try { |
253
|
|
|
$response = Craft::$app->runAction('templates/render-error'); |
254
|
|
|
} catch (InvalidRouteException $e) { |
255
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
256
|
|
|
} catch (\yii\console\Exception $e) { |
257
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
// Redirect the request away; |
261
|
|
|
$response->redirect($dest, $status)->send(); |
262
|
|
|
try { |
263
|
|
|
Craft::$app->end(); |
264
|
|
|
} catch (ExitException $e) { |
265
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return false; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @param string $fullUrl |
274
|
|
|
* @param string $pathOnly |
275
|
|
|
* @param null $siteId |
|
|
|
|
276
|
|
|
* |
277
|
|
|
* @return array|null |
278
|
|
|
*/ |
279
|
|
|
public function findRedirectMatch(string $fullUrl, string $pathOnly, $siteId = null) |
280
|
|
|
{ |
281
|
|
|
// Get the current site |
282
|
|
|
if ($siteId === null) { |
|
|
|
|
283
|
|
|
$currentSite = Craft::$app->getSites()->currentSite; |
284
|
|
|
if ($currentSite) { |
285
|
|
|
$siteId = $currentSite->id; |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
// Try getting the full URL redirect from the cache |
289
|
|
|
$redirect = $this->getRedirectFromCache($fullUrl, $siteId); |
|
|
|
|
290
|
|
|
if ($redirect) { |
291
|
|
|
$this->incrementRedirectHitCount($redirect); |
292
|
|
|
$this->saveRedirectToCache($fullUrl, $redirect); |
293
|
|
|
|
294
|
|
|
return $redirect; |
295
|
|
|
} |
296
|
|
|
// Try getting the path only redirect from the cache |
297
|
|
|
$redirect = $this->getRedirectFromCache($pathOnly, $siteId); |
298
|
|
|
if ($redirect) { |
299
|
|
|
$this->incrementRedirectHitCount($redirect); |
300
|
|
|
$this->saveRedirectToCache($pathOnly, $redirect); |
301
|
|
|
|
302
|
|
|
return $redirect; |
303
|
|
|
} |
304
|
|
|
// Resolve static redirects |
305
|
|
|
$redirects = $this->getAllStaticRedirects(null, $siteId); |
306
|
|
|
$redirect = $this->resolveRedirect($fullUrl, $pathOnly, $redirects); |
307
|
|
|
if ($redirect) { |
308
|
|
|
return $redirect; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
return null; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* @param $url |
316
|
|
|
* @param int|null $siteId |
317
|
|
|
* |
318
|
|
|
* @return bool|array |
319
|
|
|
*/ |
320
|
|
|
public function getRedirectFromCache($url, int $siteId = 0) |
321
|
|
|
{ |
322
|
|
|
$cache = Craft::$app->getCache(); |
323
|
|
|
$cacheKey = $this::CACHE_KEY.md5($url).$siteId; |
324
|
|
|
$redirect = $cache->get($cacheKey); |
325
|
|
|
Craft::info( |
326
|
|
|
Craft::t( |
327
|
|
|
'retour', |
328
|
|
|
'Cached redirect hit for {url}', |
329
|
|
|
['url' => $url] |
330
|
|
|
), |
331
|
|
|
__METHOD__ |
332
|
|
|
); |
333
|
|
|
|
334
|
|
|
return $redirect; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @param string $url |
339
|
|
|
* @param array $redirect |
340
|
|
|
*/ |
341
|
|
|
public function saveRedirectToCache($url, $redirect) |
342
|
|
|
{ |
343
|
|
|
$cache = Craft::$app->getCache(); |
344
|
|
|
// Get the current site id |
345
|
|
|
$sites = Craft::$app->getSites(); |
346
|
|
|
try { |
347
|
|
|
$siteId = $sites->getCurrentSite()->id; |
348
|
|
|
} catch (SiteNotFoundException $e) { |
349
|
|
|
$siteId = 1; |
350
|
|
|
} |
351
|
|
|
$cacheKey = $this::CACHE_KEY.md5($url).$siteId; |
352
|
|
|
// Create the dependency tags |
353
|
|
|
$dependency = new TagDependency([ |
354
|
|
|
'tags' => [ |
355
|
|
|
$this::GLOBAL_REDIRECTS_CACHE_TAG, |
356
|
|
|
$this::GLOBAL_REDIRECTS_CACHE_TAG.$siteId, |
357
|
|
|
], |
358
|
|
|
]); |
359
|
|
|
$cache->set($cacheKey, $redirect, Retour::$cacheDuration, $dependency); |
360
|
|
|
Craft::info( |
361
|
|
|
Craft::t( |
362
|
|
|
'retour', |
363
|
|
|
'Cached redirect saved for {url}', |
364
|
|
|
['url' => $url] |
365
|
|
|
), |
366
|
|
|
__METHOD__ |
367
|
|
|
); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* @param string $fullUrl |
372
|
|
|
* @param string $pathOnly |
373
|
|
|
* @param array $redirects |
374
|
|
|
* |
375
|
|
|
* @return array|null |
376
|
|
|
*/ |
377
|
|
|
public function resolveRedirect(string $fullUrl, string $pathOnly, array $redirects) |
378
|
|
|
{ |
379
|
|
|
$result = null; |
380
|
|
|
// Throw the Redirects::EVENT_BEFORE_RESOLVE_REDIRECT event |
381
|
|
|
$event = new ResolveRedirectEvent([ |
382
|
|
|
'fullUrl' => $fullUrl, |
383
|
|
|
'pathOnly' => $pathOnly, |
384
|
|
|
'redirectDestUrl' => null, |
385
|
|
|
'redirectHttpCode' => 301, |
386
|
|
|
]); |
387
|
|
|
$this->trigger(self::EVENT_BEFORE_RESOLVE_REDIRECT, $event); |
388
|
|
|
if ($event->redirectDestUrl !== null) { |
389
|
|
|
return $this->resolveEventRedirect($event); |
390
|
|
|
} |
391
|
|
|
// Iterate through the redirects |
392
|
|
|
foreach ($redirects as $redirect) { |
393
|
|
|
// Figure out what type of source matching to do |
394
|
|
|
$redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly'; |
395
|
|
|
$redirectEnabled = (bool)$redirect['enabled']; |
396
|
|
|
if ($redirectEnabled === true) { |
397
|
|
|
switch ($redirectSrcMatch) { |
398
|
|
|
case 'pathonly': |
399
|
|
|
$url = $pathOnly; |
400
|
|
|
break; |
401
|
|
|
case 'fullurl': |
402
|
|
|
$url = $fullUrl; |
403
|
|
|
break; |
404
|
|
|
default: |
405
|
|
|
$url = $pathOnly; |
406
|
|
|
break; |
407
|
|
|
} |
408
|
|
|
$redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound'; |
409
|
|
|
switch ($redirectMatchType) { |
410
|
|
|
// Do a straight up match |
411
|
|
|
case 'exactmatch': |
412
|
|
|
if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) { |
413
|
|
|
$this->incrementRedirectHitCount($redirect); |
414
|
|
|
$this->saveRedirectToCache($url, $redirect); |
415
|
|
|
|
416
|
|
|
return $redirect; |
417
|
|
|
} |
418
|
|
|
break; |
419
|
|
|
|
420
|
|
|
// Do a regex match |
421
|
|
|
case 'regexmatch': |
422
|
|
|
$matchRegEx = '`'.$redirect['redirectSrcUrlParsed'].'`i'; |
423
|
|
|
try { |
424
|
|
|
if (preg_match($matchRegEx, $url) === 1) { |
425
|
|
|
$this->incrementRedirectHitCount($redirect); |
426
|
|
|
// If we're not associated with an EntryID, handle capture group replacement |
427
|
|
|
if ((int)$redirect['associatedElementId'] === 0) { |
428
|
|
|
$redirect['redirectDestUrl'] = preg_replace( |
429
|
|
|
$matchRegEx, |
430
|
|
|
$redirect['redirectDestUrl'], |
431
|
|
|
$url |
432
|
|
|
); |
433
|
|
|
} |
434
|
|
|
$this->saveRedirectToCache($url, $redirect); |
435
|
|
|
|
436
|
|
|
return $redirect; |
437
|
|
|
} |
438
|
|
|
} catch (\Exception $e) { |
439
|
|
|
// That's fine |
440
|
|
|
Craft::error('Invalid Redirect Regex: '.$matchRegEx, __METHOD__); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
break; |
444
|
|
|
|
445
|
|
|
// Otherwise try to look up a plugin's method by and call it for the match |
446
|
|
|
default: |
447
|
|
|
$plugin = $redirectMatchType ? Craft::$app->getPlugins()->getPlugin($redirectMatchType) : null; |
448
|
|
|
if ($plugin && method_exists($plugin, 'retourMatch')) { |
449
|
|
|
$args = [ |
450
|
|
|
[ |
451
|
|
|
'redirect' => &$redirect, |
452
|
|
|
], |
453
|
|
|
]; |
454
|
|
|
$result = \call_user_func_array([$plugin, 'retourMatch'], $args); |
455
|
|
|
if ($result) { |
456
|
|
|
$this->incrementRedirectHitCount($redirect); |
457
|
|
|
$this->saveRedirectToCache($url, $redirect); |
458
|
|
|
|
459
|
|
|
return $redirect; |
460
|
|
|
} |
461
|
|
|
} |
462
|
|
|
break; |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
// Throw the Redirects::EVENT_AFTER_RESOLVE_REDIRECT event |
467
|
|
|
$event = new ResolveRedirectEvent([ |
468
|
|
|
'fullUrl' => $fullUrl, |
469
|
|
|
'pathOnly' => $pathOnly, |
470
|
|
|
'redirectDestUrl' => null, |
471
|
|
|
'redirectHttpCode' => 301, |
472
|
|
|
]); |
473
|
|
|
$this->trigger(self::EVENT_AFTER_RESOLVE_REDIRECT, $event); |
474
|
|
|
if ($event->redirectDestUrl !== null) { |
475
|
|
|
return $this->resolveEventRedirect($event); |
476
|
|
|
} |
477
|
|
|
Craft::info( |
478
|
|
|
Craft::t( |
479
|
|
|
'retour', |
480
|
|
|
'Not handled-> full URL: {fullUrl}, path only: {pathOnly}', |
481
|
|
|
['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly] |
482
|
|
|
), |
483
|
|
|
__METHOD__ |
484
|
|
|
); |
485
|
|
|
|
486
|
|
|
return $result; |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
/** |
490
|
|
|
* @param ResolveRedirectEvent $event |
491
|
|
|
* |
492
|
|
|
* @return null|array |
493
|
|
|
*/ |
494
|
|
|
public function resolveEventRedirect(ResolveRedirectEvent $event) |
495
|
|
|
{ |
496
|
|
|
$result = null; |
497
|
|
|
|
498
|
|
|
if ($event->redirectDestUrl !== null) { |
499
|
|
|
$redirect = new StaticRedirectsModel([ |
500
|
|
|
'id' => self::EVENT_REDIRECT_ID, |
501
|
|
|
'redirectDestUrl' => $event->redirectDestUrl, |
502
|
|
|
'redirectHttpCode' => $event->redirectHttpCode, |
503
|
|
|
]); |
504
|
|
|
$result = $redirect->toArray(); |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
return $result; |
508
|
|
|
} |
509
|
|
|
|
510
|
|
|
/** |
511
|
|
|
* Returns the list of matching schemes |
512
|
|
|
* |
513
|
|
|
* @return array |
514
|
|
|
*/ |
515
|
|
|
public function getMatchesList(): array |
516
|
|
|
{ |
517
|
|
|
$result = [ |
518
|
|
|
'exactmatch' => Craft::t('retour', 'Exact Match'), |
519
|
|
|
'regexmatch' => Craft::t('retour', 'RegEx Match'), |
520
|
|
|
]; |
521
|
|
|
|
522
|
|
|
// Add any plugins that offer the retourMatch() method |
523
|
|
|
foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) { |
524
|
|
|
/** @var Plugin $plugin */ |
525
|
|
|
if (method_exists($plugin, 'retourMatch')) { |
526
|
|
|
$result[$plugin->getHandle()] = $plugin->name.Craft::t('retour', ' Match'); |
527
|
|
|
} |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
return $result; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* @param null|int $limit |
535
|
|
|
* @param int|null $siteId |
536
|
|
|
* |
537
|
|
|
* @return array All of the statistics |
538
|
|
|
*/ |
539
|
|
|
public function getAllStaticRedirects($limit = null, int $siteId = null): array |
540
|
|
|
{ |
541
|
|
|
// Cache it in our class; no need to fetch it more than once |
542
|
|
|
if ($this->cachedStaticRedirects !== null) { |
543
|
|
|
return $this->cachedStaticRedirects; |
544
|
|
|
} |
545
|
|
|
// Query the db table |
546
|
|
|
$query = (new Query()) |
547
|
|
|
->from(['{{%retour_static_redirects}}']) |
548
|
|
|
->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC'); |
549
|
|
|
if ($siteId) { |
|
|
|
|
550
|
|
|
$query |
551
|
|
|
->where(['siteId' => $siteId]) |
552
|
|
|
->orWhere(['siteId' => null]); |
553
|
|
|
} |
554
|
|
|
if ($limit) { |
|
|
|
|
555
|
|
|
$query->limit($limit); |
556
|
|
|
} |
557
|
|
|
$redirects = $query->all(); |
558
|
|
|
// Cache for future accesses |
559
|
|
|
$this->cachedStaticRedirects = $redirects; |
560
|
|
|
|
561
|
|
|
return $redirects; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Return a redirect by id |
566
|
|
|
* |
567
|
|
|
* @param int $id |
568
|
|
|
* |
569
|
|
|
* @return null|array The static redirect |
570
|
|
|
*/ |
571
|
|
|
public function getRedirectById(int $id) |
572
|
|
|
{ |
573
|
|
|
// Query the db table |
574
|
|
|
$redirect = (new Query()) |
575
|
|
|
->from(['{{%retour_static_redirects}}']) |
576
|
|
|
->where(['id' => $id]) |
577
|
|
|
->one(); |
578
|
|
|
|
579
|
|
|
return $redirect; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* Return a redirect by redirectSrcUrl |
584
|
|
|
* |
585
|
|
|
* @param string $redirectSrcUrl |
586
|
|
|
* @param int|null $siteId |
587
|
|
|
* |
588
|
|
|
* @return null|array |
589
|
|
|
*/ |
590
|
|
|
public function getRedirectByRedirectSrcUrl(string $redirectSrcUrl, int $siteId = null) |
591
|
|
|
{ |
592
|
|
|
// Query the db table |
593
|
|
|
$query = (new Query()) |
594
|
|
|
->from(['{{%retour_static_redirects}}']) |
595
|
|
|
->where(['redirectSrcUrl' => $redirectSrcUrl]) |
596
|
|
|
; |
597
|
|
|
if ($siteId) { |
|
|
|
|
598
|
|
|
$query |
599
|
|
|
->andWhere(['or', [ |
600
|
|
|
'siteId' => $siteId, |
601
|
|
|
], [ |
602
|
|
|
'siteId' => null, |
603
|
|
|
]]); |
604
|
|
|
} |
605
|
|
|
$redirect = $query->one(); |
606
|
|
|
|
607
|
|
|
return $redirect; |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
/** |
611
|
|
|
* Delete a redirect by id |
612
|
|
|
* |
613
|
|
|
* @param int $id |
614
|
|
|
* |
615
|
|
|
* @return int The result |
616
|
|
|
*/ |
617
|
|
|
public function deleteRedirectById(int $id): int |
618
|
|
|
{ |
619
|
|
|
$db = Craft::$app->getDb(); |
620
|
|
|
// Delete a row from the db table |
621
|
|
|
try { |
622
|
|
|
$result = $db->createCommand()->delete( |
623
|
|
|
'{{%retour_static_redirects}}', |
624
|
|
|
[ |
625
|
|
|
'id' => $id, |
626
|
|
|
] |
627
|
|
|
)->execute(); |
628
|
|
|
} catch (Exception $e) { |
629
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
630
|
|
|
$result = 0; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
return $result; |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Increment the retour_static_redirects record |
638
|
|
|
* |
639
|
|
|
* @param $redirectConfig |
640
|
|
|
*/ |
641
|
|
|
public function incrementRedirectHitCount(&$redirectConfig) |
642
|
|
|
{ |
643
|
|
|
if ($redirectConfig !== null) { |
644
|
|
|
$db = Craft::$app->getDb(); |
645
|
|
|
$redirectConfig['hitCount']++; |
646
|
|
|
$redirectConfig['hitLastTime'] = Db::prepareDateForDb(new \DateTime()); |
647
|
|
|
Craft::debug( |
648
|
|
|
Craft::t( |
649
|
|
|
'retour', |
650
|
|
|
'Incrementing statistics for: {redirect}', |
651
|
|
|
['redirect' => print_r($redirectConfig, true)] |
652
|
|
|
), |
653
|
|
|
__METHOD__ |
654
|
|
|
); |
655
|
|
|
// Update the existing record |
656
|
|
|
try { |
657
|
|
|
$rowsAffected = $db->createCommand()->update( |
658
|
|
|
'{{%retour_static_redirects}}', |
659
|
|
|
[ |
660
|
|
|
'hitCount' => $redirectConfig['hitCount'], |
661
|
|
|
'hitLastTime' => $redirectConfig['hitLastTime'], |
662
|
|
|
], |
663
|
|
|
[ |
664
|
|
|
'id' => $redirectConfig['id'], |
665
|
|
|
] |
666
|
|
|
)->execute(); |
667
|
|
|
Craft::debug('Rows affected: '.$rowsAffected, __METHOD__); |
668
|
|
|
} catch (\Exception $e) { |
669
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
670
|
|
|
} |
671
|
|
|
} |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
/** |
675
|
|
|
* @param array $redirectConfig |
676
|
|
|
*/ |
677
|
|
|
public function saveRedirect(array $redirectConfig) |
678
|
|
|
{ |
679
|
|
|
// Validate the model before saving it to the db |
680
|
|
|
$redirect = new StaticRedirectsModel($redirectConfig); |
681
|
|
|
if ($redirect->validate() === false) { |
682
|
|
|
Craft::error( |
683
|
|
|
Craft::t( |
684
|
|
|
'retour', |
685
|
|
|
'Error validating redirect {id}: {errors}', |
686
|
|
|
['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)] |
687
|
|
|
), |
688
|
|
|
__METHOD__ |
689
|
|
|
); |
690
|
|
|
|
691
|
|
|
return; |
692
|
|
|
} |
693
|
|
|
// Get the validated model attributes and save them to the db |
694
|
|
|
$redirectConfig = $redirect->getAttributes(); |
695
|
|
|
// 0 for a siteId needs to be converted to null |
696
|
|
|
if (empty($redirectConfig['siteId']) || (int)$redirectConfig['siteId'] === 0) { |
697
|
|
|
$redirectConfig['siteId'] = null; |
698
|
|
|
} |
699
|
|
|
// Throw an event to before saving the redirect |
700
|
|
|
$db = Craft::$app->getDb(); |
701
|
|
|
// See if a redirect exists with this source URL already |
702
|
|
|
if ((int)$redirectConfig['id'] === 0) { |
703
|
|
|
// Query the db table |
704
|
|
|
$redirect = (new Query()) |
705
|
|
|
->from(['{{%retour_static_redirects}}']) |
706
|
|
|
->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']]) |
707
|
|
|
->andWhere(['siteId' => $redirectConfig['siteId']]) |
708
|
|
|
->one(); |
709
|
|
|
// If it exists, update it rather than having duplicates |
710
|
|
|
if (!empty($redirect)) { |
711
|
|
|
$redirectConfig['id'] = $redirect['id']; |
712
|
|
|
} |
713
|
|
|
} |
714
|
|
|
// Trigger a 'beforeSaveRedirect' event |
715
|
|
|
$isNew = (int)$redirectConfig['id'] === 0; |
716
|
|
|
$event = new RedirectEvent([ |
717
|
|
|
'isNew' => $isNew, |
718
|
|
|
'legacyUrl' => $redirectConfig['redirectSrcUrlParsed'], |
719
|
|
|
'destinationUrl' => $redirectConfig['redirectDestUrl'], |
720
|
|
|
'matchType' => $redirectConfig['redirectSrcMatch'], |
721
|
|
|
'redirectType' => $redirectConfig['redirectHttpCode'], |
722
|
|
|
]); |
723
|
|
|
$this->trigger(self::EVENT_BEFORE_SAVE_REDIRECT, $event); |
724
|
|
|
if (!$event->isValid) { |
725
|
|
|
return; |
726
|
|
|
} |
727
|
|
|
// See if this is an existing redirect |
728
|
|
|
if (!$isNew) { |
729
|
|
|
Craft::debug( |
730
|
|
|
Craft::t( |
731
|
|
|
'retour', |
732
|
|
|
'Updating existing redirect: {redirect}', |
733
|
|
|
['redirect' => print_r($redirectConfig, true)] |
734
|
|
|
), |
735
|
|
|
__METHOD__ |
736
|
|
|
); |
737
|
|
|
// Update the existing record |
738
|
|
|
try { |
739
|
|
|
$db->createCommand()->update( |
740
|
|
|
'{{%retour_static_redirects}}', |
741
|
|
|
$redirectConfig, |
742
|
|
|
[ |
743
|
|
|
'id' => $redirectConfig['id'], |
744
|
|
|
] |
745
|
|
|
)->execute(); |
746
|
|
|
} catch (Exception $e) { |
747
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
748
|
|
|
} |
749
|
|
|
} else { |
750
|
|
|
Craft::debug( |
751
|
|
|
Craft::t( |
752
|
|
|
'retour', |
753
|
|
|
'Creating new redirect: {redirect}', |
754
|
|
|
['redirect' => print_r($redirectConfig, true)] |
755
|
|
|
), |
756
|
|
|
__METHOD__ |
757
|
|
|
); |
758
|
|
|
unset($redirectConfig['id']); |
759
|
|
|
// Create a new record |
760
|
|
|
try { |
761
|
|
|
$db->createCommand()->insert( |
762
|
|
|
'{{%retour_static_redirects}}', |
763
|
|
|
$redirectConfig |
764
|
|
|
)->execute(); |
765
|
|
|
} catch (Exception $e) { |
766
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
767
|
|
|
} |
768
|
|
|
} |
769
|
|
|
// To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl |
770
|
|
|
$testRedirectConfig = $this->getRedirectByRedirectSrcUrl( |
771
|
|
|
$redirectConfig['redirectDestUrl'], |
772
|
|
|
$redirectConfig['siteId'] |
773
|
|
|
); |
774
|
|
|
if ($testRedirectConfig !== null) { |
775
|
|
|
Craft::debug( |
776
|
|
|
Craft::t( |
777
|
|
|
'retour', |
778
|
|
|
'Deleting redirect to prevent a loop: {redirect}', |
779
|
|
|
['redirect' => print_r($testRedirectConfig, true)] |
780
|
|
|
), |
781
|
|
|
__METHOD__ |
782
|
|
|
); |
783
|
|
|
// Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl |
784
|
|
|
try { |
785
|
|
|
$db->createCommand()->delete( |
786
|
|
|
'{{%retour_static_redirects}}', |
787
|
|
|
['id' => $testRedirectConfig['id']] |
788
|
|
|
)->execute(); |
789
|
|
|
} catch (Exception $e) { |
790
|
|
|
Craft::error($e->getMessage(), __METHOD__); |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
// Trigger a 'afterSaveRedirect' event |
794
|
|
|
$this->trigger(self::EVENT_AFTER_SAVE_REDIRECT, $event); |
795
|
|
|
} |
796
|
|
|
|
797
|
|
|
/** |
798
|
|
|
* Invalidate all of the redirects caches |
799
|
|
|
*/ |
800
|
|
|
public function invalidateCaches() |
801
|
|
|
{ |
802
|
|
|
$cache = Craft::$app->getCache(); |
803
|
|
|
TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG); |
804
|
|
|
Craft::info( |
805
|
|
|
Craft::t( |
806
|
|
|
'retour', |
807
|
|
|
'All redirect caches cleared' |
808
|
|
|
), |
809
|
|
|
__METHOD__ |
810
|
|
|
); |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
/** |
814
|
|
|
* @param $uri |
815
|
|
|
* |
816
|
|
|
* @return bool |
817
|
|
|
*/ |
818
|
|
|
public function excludeUri($uri): bool |
819
|
|
|
{ |
820
|
|
|
$uri = '/'.ltrim($uri, '/'); |
821
|
|
|
if (!empty(Retour::$settings->excludePatterns)) { |
822
|
|
|
foreach (Retour::$settings->excludePatterns as $excludePattern) { |
823
|
|
|
$pattern = '`'.$excludePattern['pattern'].'`i'; |
824
|
|
|
try { |
825
|
|
|
if (preg_match($pattern, $uri) === 1) { |
826
|
|
|
return true; |
827
|
|
|
} |
828
|
|
|
} catch (\Exception $e) { |
829
|
|
|
// That's fine |
830
|
|
|
Craft::error('Invalid exclude URI Regex: '.$pattern, __METHOD__); |
831
|
|
|
} |
832
|
|
|
} |
833
|
|
|
} |
834
|
|
|
|
835
|
|
|
return false; |
836
|
|
|
} |
837
|
|
|
} |
838
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.