Skip to content

Commit

Permalink
✨ added appendRedirects and removeRedirects site-methods
Browse files Browse the repository at this point in the history
Signed-off-by: Bruno Meilick <b@bnomei.com>
  • Loading branch information
bnomei committed Dec 20, 2019
1 parent 5158d03 commit c8396ed
Show file tree
Hide file tree
Showing 14 changed files with 982 additions and 1,513 deletions.
32 changes: 31 additions & 1 deletion README.md
Expand Up @@ -53,13 +53,43 @@ sections:
> Since v1.1.0 the plugin will register itself with a `route:before`-hook and take care of the redirecting automatically. Many thanks to _Sebastian Aschenbach_ for suggesting this solution.
## Site Methods

The site methods `appendRedirect` and `removeRedirect` allow you to programmatically change the redirects table (if stored in a Page/Site-Object).

```php
// add single item
$success = site()->appendRedirects(
['fromuri'=>'/posts?id=1', 'touri'=>'/blog/1', 'code'=>301]
);

// add multiple items with nested array
$success = site()->appendRedirects([
['fromuri'=>'/posts?id=2', 'touri'=>'/blog/2', 'code'=>301],
// ...
['fromuri'=>'/posts?id=999', 'touri'=>'/blog/999', 'code'=>301],
]);

// remove single item
$success = site()->removeRedirects(
['fromuri'=>'/posts?id=1', 'touri'=>'/blog/1']
);

// remove multiple items with nested array
$success = site()->removeRedirects([
['fromuri'=>'/posts?id=3', 'touri'=>'/blog/3'],
['fromuri'=>'/posts?id=5', 'touri'=>'/blog/5'],
['fromuri'=>'/posts?id=7', 'touri'=>'/blog/7'],
]);
```

## Settings

| bnomei.redirects. | Default | Description |
|---------------------------|----------------|---------------------------|
| code | `301` | |
| querystring | `true` | do keep querystring in request URI. example: `https://kirby3-plugins.bnomei.com/projects?id=12` => `projects?id=12` |
| map | `callback` | A closure to get the structure from `site.txt`. Define you own if you want the section to be in a different blueprint or skip the blueprint and just use code. |
| map | `callback` | A closure to get the structure from `content/site.txt`. Define you own if you want the section to be in a different blueprint or skip the blueprint and just use code. |

## Disclaimer

Expand Down
91 changes: 91 additions & 0 deletions classes/Redirect.php
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Bnomei;

use Kirby\Toolkit\V;

final class Redirect
{
/**
* @var string
*/
private $fromuri;
/**
* @var string
*/
private $touri;
/**
* @var int
*/
private $code;

public function __construct(string $fromuri, string $touri, $code = 301)
{
$this->fromuri = $fromuri;
$this->touri = $this->url($touri);
$this->code = static::normalizeCode($code);
}

public function matches(string $url): bool
{
return $this->from() === $url;
}

public function from(): string
{
return $this->fromuri;
}

public function to(): string
{
return $this->touri;
}

public function code(): int
{
return $this->code;
}

public function toArray(): array
{
return [
'fromuri' => $this->from(),
'touri' => $this->to(),
'code' => '_' . $this->code(),
];
}

public function __debugInfo()
{
return $this->toArray();
}

public static function url($url): string
{
$id = '/' . trim($url, '/');
$page = page($id);
if ($page) {
return $page->url();
}

if (V::url($url)) {
return $url;
}

return url($url);
}

public static function normalizeCode($code): int
{
if (is_string($code)) {
$code = intval(str_replace('_', '', $code));
}
if (! $code) {
$code = 301;
}

return $code;
}
}
171 changes: 123 additions & 48 deletions classes/Redirects.php
Expand Up @@ -4,6 +4,10 @@

namespace Bnomei;

use Kirby\Cms\Field;
use Kirby\Cms\Page;
use Kirby\Cms\Site;
use Kirby\Data\Yaml;
use Kirby\Http\Header;
use Kirby\Http\Url;
use Kirby\Toolkit\A;
Expand All @@ -19,19 +23,20 @@ final class Redirects
public function __construct(array $options = [])
{
$defaults = [
'code' => $this->normalizeCode(option('bnomei.redirects.code')),
'code' => option('bnomei.redirects.code'),
'querystring' => option('bnomei.redirects.querystring'),
'map' => option('bnomei.redirects.map', []),
'map' => option('bnomei.redirects.map'),
'site.url' => site()->url(), // a) www.example.com or b) www.example.com/subfolder
'request.uri' => $this->getRequestURI(),
];
$this->options = array_merge($defaults, $options);

foreach ($this->options as $key => $call) {
if (is_callable($call)) {
if (is_callable($call) && in_array($key, ['code', 'querystring', 'map'])) {
$this->options[$key] = $call();
}
}
$this->options['redirects'] = $this->map($this->options['map']);

$this->checkForRedirect($this->options);
}
Expand All @@ -44,82 +49,152 @@ public function option(?string $key = null)
return $this->options;
}

public function checkForRedirect(array $options): ?array
public function map($redirects = null)
{
$map = A::get($options, 'map');
if (is_a($redirects, Field::class)) {
return $redirects->isNotEmpty() ? $redirects->yaml() : [];
}
return is_array($redirects) ? $redirects : [];
}

public function append(array $change): bool
{
if (is_array($change) &&
count($change) === count($change, COUNT_RECURSIVE)
) {
$change = [$change];
}

$code = $this->option('code');
$change = array_map(function ($v) use ($code) {
$redirect = new Redirect(
A::get($v, 'fromuri'),
A::get($v, 'touri'),
A::get($v, 'code', $code)
);
return $redirect->toArray();
}, $change);

$data = array_merge(
$this->option('redirects'),
$change
);
$this->options['redirects'] = $data;
return $this->updateRedirects($data);
}

public function remove(array $change): bool
{
if (is_array($change) &&
count($change) === count($change, COUNT_RECURSIVE)
) {
$change = [$change];
}

$data = $this->option('redirects');
$copy = $data;
foreach($change as $item) {
foreach ($copy as $key => $redirect) {
if (A::get($redirect, 'fromuri') === A::get($item, 'fromuri') &&
A::get($redirect, 'touri') === A::get($item, 'touri')) {
unset($data[$key]);
break; // exit inner loop
}
}
}
$this->options['redirects'] = $data;
return $this->updateRedirects($data);
}

public function updateRedirects(array $data): bool
{
$map = $this->option('map');
if (is_a($map, Field::class)) {
$page = $map->parent();
if (is_a($page, Site::class) ||
is_a($page, Page::class)
) {
try {
kirby()->impersonate('kirby');
$page->update([
$map->key() => Yaml::encode($data),
]);
return true;
// @codeCoverageIgnoreStart
} catch (\Exception $ex) { }
// @codeCoverageIgnoreEnd
}
}
return false;
}

public function checkForRedirect(): ?Redirect
{
$map = $this->option('redirects');
if (! $map || count($map) === 0) {
return null;
}

$siteurl = A::get($options, 'site.url');
$requesturi = A::get($options, 'request.uri');
$requesturi = (string) $this->option('request.uri');

foreach ($map as $redirect) {
if ($this->matchesFromUri($redirect, $requesturi, $siteurl)) {
return [
'uri' => $this->validateToUri($redirect),
'code' => $this->validateCode($redirect, A::get($options, 'code')),
];
if (!array_key_exists('fromuri', $redirect) ||
!array_key_exists('touri', $redirect)
) {
continue;
}
$redirect = new Redirect(
$this->makeRelativePath(A::get($redirect, 'fromuri', '')),
A::get($redirect, 'touri', ''),
A::get($redirect, 'code', $this->option('code'))
);

if ($redirect->matches($requesturi)) {
return $redirect;
}
}
return null;
}

public function matchesFromUri(array $redirect, string $requesturi, string $siteurl): bool
private function makeRelativePath(string $url)
{
$siteurl = A::get($this->options, 'site.url');
$sitebase = Url::path($siteurl, true, true);
$fromuri = A::get($redirect, 'fromuri');
$fromuri = '/' . trim($sitebase . str_replace($siteurl, '', $fromuri), '/');
return $fromuri === $requesturi;
$url = str_replace($siteurl, '', $url);

return '/' . trim($sitebase . $url, '/');
}

private function getRequestURI(): string
{
$uri = array_key_exists("REQUEST_URI", $_SERVER) ? $_SERVER["REQUEST_URI"] : '/' . kirby()->request()->path();
$uri = option('bnomei.redirects.querystring') ? $uri : strtok($uri, '?'); // / or /page or /subfolder or /subfolder/page

return $uri;
}

private function normalizeCode($code): int
public function redirect()
{
return intval(str_replace('_', '', $code));
}
$check = $this->checkForRedirect();

private function validateToUri($redirect): string
{
$touri = '/' . trim(A::get($redirect, 'touri'), '/');
$page = page($touri);
if ($page) {
$touri = $page->url();
} else {
$touri = url($touri);
if ($check) {
// @codeCoverageIgnoreStart
Header::redirect($check->to(), $check->code());
// @codeCoverageIgnoreEnd
}
return $touri;
}

private function validateCode(array $redirect, int $optionsCode): int
{
$redirectCode = $this->normalizeCode(A::get($redirect, 'code'));
if (! $redirectCode || $redirectCode === 0) {
$redirectCode = $optionsCode;
}
return $redirectCode;
}
private static $singleton;

public static function redirects($options = [])
public static function singleton($options = []): Redirects
{
$redirects = new self($options);
$check = $redirects->checkForRedirect(
$redirects->option()
);
if ($check && is_array($check)
&& array_key_exists('uri', $check)
&& array_key_exists('code', $check)
) {
// @codeCoverageIgnoreStart
Header::redirect($check['uri'], $check['code']);
// @codeCoverageIgnoreEnd
// @codeCoverageIgnoreStart
if (! self::$singleton) {
self::$singleton = new self($options);
}
// @codeCoverageIgnoreEnd

return self::$singleton;
}

public static function codes(bool $force = false): ?array
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Expand Up @@ -2,7 +2,7 @@
"name": "bnomei/kirby3-redirects",
"type": "kirby-plugin",
"description": "Setup HTTP Status Code Redirects from within the Kirby Panel",
"version": "1.4.3",
"version": "1.5.0",
"license": "MIT",
"authors": [
{
Expand Down Expand Up @@ -43,6 +43,12 @@
"dist": [
"composer install --no-dev --optimize-autoloader",
"git rm -rf --cached .; git add .;"
],
"kirby": [
"composer install",
"composer update",
"composer install --working-dir=tests/kirby --no-dev --optimize-autoloader",
"composer update --working-dir=tests/kirby"
]
},
"require": {
Expand Down

0 comments on commit c8396ed

Please sign in to comment.