Commit 815b0552 authored by Sean Madsen's avatar Sean Madsen

Merge branch 'page-redirection' into 'master'

Page redirection

Closes #20

See merge request !69
parents 9a8d18b5 2b212a93
......@@ -42,3 +42,10 @@ services:
class: Monolog\Handler\StreamHandler
arguments:
- %kernel.logs_dir%/publish.log
app.exception_listener:
class: AppBundle\EventListener\ExceptionListener
arguments:
- %publish_path_root%
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
<?php
namespace AppBundle\EventListener;
use AppBundle\Utils\StringTools;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use \AppBundle\Model\Library;
class ExceptionListener {
/**
* @var string Filesystem path to the directory where all published books go
*/
public $publishPathRoot;
/**
* ExceptionListener constructor.
* @param string $publishPathRoot
*/
public function __construct($publishPathRoot) {
$this->publishPathRoot = $publishPathRoot;
}
/**
* This method is called by some sort of Symfony magic for every exception
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
*/
public function onKernelException(GetResponseForExceptionEvent $event) {
$exception = $event->getException();
if ($exception instanceof NotFoundHttpException) {
$requestUri = $event->getRequest()->getRequestUri();
$redirect = $this->lookupRedirect($requestUri);
if ($redirect) {
$response = new RedirectResponse($redirect);
$event->setResponse($response);
}
}
}
/**
* See if we have a redirect stored for the given URI. If so, return the full
* path to it as a string (which begins with a slash). If not, return NULL
* @param string $requestUri
* @return null|string
*/
private function lookupRedirect($requestUri) {
// Give up right away if the request contains two dots (for security)
if (strstr($requestUri, '..')) {
return NULL;
}
$requestParts = Library::parseIdentifier($requestUri);
$edition = $requestParts['editionIdentifier'];
$path = $requestParts['path'];
$fragment = $requestParts['fragment'] ? "#${requestParts['fragment']}" : '';
$redirectsFile = $this->publishPathRoot . '/' . $edition . '/redirects.txt';
// If we don't have all the info we need, then give up
if ($edition === NULL || $path === NULL || !file_exists($redirectsFile)) {
return NULL;
}
// Look for a redirect
$redirects = file($redirectsFile);
foreach ($redirects as $redirect) {
$rule = StringTools::parseRedirectRule($redirect);
if ($rule && $rule['from'] == $path) {
return "/$edition/${rule['to']}$fragment";
}
}
return NULL;
}
}
......@@ -16,7 +16,7 @@ class Library {
/**
* Build a new Library based on a directory of book conf files.
*
* @param strig $configDir
* @param string $configDir
*/
public function __construct($configDir) {
$finder = new Finder();
......@@ -147,18 +147,38 @@ class Library {
*
* @param string $identifier (e.g. "user/en/master", "user/en", "user", "")
*
* @return array The following keys/values are present:
* "bookSlug" => (string/NULL) slug used to identify a book
* "languageCode" => (string/NULL) two letter language code
* "versionDescriptor" => (string/NULL)
* @return array See LibraryTest::identifierProvider() for examples
*/
public static function parseIdentifier($identifier) {
$identifier = preg_replace("#/+#", "/", trim($identifier));
$identifier = trim($identifier, "/");
$parts = explode("/", $identifier);
$result['bookSlug'] = ($parts[0]) ? $parts[0] : NULL;
$result['languageCode'] = isset($parts[1]) ? $parts[1] : NULL;
$result['versionDescriptor'] = isset($parts[2]) ? $parts[2] : NULL;
// Remove junk chars from both ends
$identifier = trim($identifier, "/# \t\n\r\0\x0B");
// Ensure there are no repeated occurrences of "/" or "#"
$identifier = preg_replace("_(/|#)+_", "$1", $identifier);
// Split into 2 parts based on the first "#" character
$hashSplit = explode('#', $identifier, 2);
$fragment = $hashSplit[1] ?? NULL;
$preFragment = $hashSplit[0] ?? NULL;
// Take everything before "#" and split it into 4 parts
$slashSplit = explode("/", $preFragment, 4);
// Assign parts
$result['bookSlug'] = $slashSplit[0] ?? NULL;
$result['languageCode'] = $slashSplit[1] ?? NULL;
$result['versionDescriptor'] = $slashSplit[2] ?? NULL;
$editionParts = [
$result['bookSlug'],
$result['languageCode'],
$result['versionDescriptor']
];
$result['editionIdentifier'] = in_array(FALSE, $editionParts)
? NULL
: implode('/', $editionParts);
$result['path'] = $slashSplit[3] ?? NULL;
$result['fragment'] = $fragment;
return $result;
}
......
......@@ -407,6 +407,21 @@ class Publisher {
return TRUE;
}
/**
* If we have an internal redirects file in the repo, then copy it to the
* path where the book is published. This app will look for redirects in this
* file when it can't find static HTML files to serve.
*
* @return bool
*/
private function setupRedirects() {
$redirectsFile = $this->repoPath . '/redirects/internal.txt';
if (file_exists($redirectsFile)) {
copy($redirectsFile, $this->publishPath . '/redirects.txt');
}
return TRUE;
}
/**
* Publish a book, or multiple books, based on a flexible identifier
*
......@@ -512,7 +527,8 @@ class Publisher {
$this->gitCheckout() &&
$this->gitPull() &&
$this->build() &&
$this->setupSymlinks();
$this->setupSymlinks() &&
$this->setupRedirects();
return $success;
}
......
......@@ -20,4 +20,36 @@ class StringTools {
return $clean;
}
/**
* Look at one redirect rule, as it is written in a text file, and determine
* the "from" and "to" elements.
*
* See StringToolsTest::redirectRuleProvider() for examples
*
* @param string $rule
*
* @return array
*/
public static function parseRedirectRule($rule) {
$rule = trim($rule);
// ignore comments (lines beginning with #)
if (preg_match('_^#_', $rule)) {
return NULL;
}
// Split by spaces
$redirectParts = array_values(array_filter(explode(' ', $rule)));
// Trim slashes from results
$redirectParts = array_map(function($v) {
return trim($v,'/');
}, $redirectParts);
$from = $redirectParts[0] ?? NULL;
$to = $redirectParts[1] ?? NULL;
return ($from && $to) ? ['from' => $from, 'to' => $to] : NULL;
}
}
<?php
namespace AppBundle\Model;
class LibraryTest extends \PHPUnit_Framework_TestCase {
/**
* @param string $identifier
* @param array $expected
* @dataProvider identifierProvider
*/
public function testParseIdentifier($identifier, $expected) {
$this->assertEquals($expected, Library::parseIdentifier($identifier));
}
public function identifierProvider() {
return [
[
'',
[
'bookSlug' => NULL,
'languageCode' => NULL,
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => NULL,
],
],
[
'/dev',
[
'bookSlug' => 'dev',
'languageCode' => NULL,
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => NULL,
],
],
[
" /dev \n",
[
'bookSlug' => 'dev',
'languageCode' => NULL,
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => NULL,
],
],
[
"/foo bar/baz bat",
[
'bookSlug' => 'foo bar',
'languageCode' => 'baz bat',
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => NULL,
],
],
[
'/dev/en',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => NULL,
],
],
[
'/dev/en/latest',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => NULL,
'fragment' => NULL,
],
],
[
'dev/en/latest',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => NULL,
'fragment' => NULL,
],
],
[
'dev/en/latest/',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => NULL,
'fragment' => NULL,
],
],
[
'//dev///////en//latest///',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => NULL,
'fragment' => NULL,
],
],
[
'dev/en/latest/category/foo/my-page/',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page',
'fragment' => NULL,
],
],
[
'dev/en/latest/category/foo/my-page#',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page',
'fragment' => NULL,
],
],
[
'dev/en/latest/category/foo/my-page/#some-section',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page',
'fragment' => 'some-section',
],
],
[
'dev/en/latest/category/foo/my-page/#some-section#another-section',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page',
'fragment' => 'some-section#another-section',
],
],
[
'dev/en/latest/category/foo/my-page#some-section',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page',
'fragment' => 'some-section',
],
],
[
'dev/en/latest/category/foo/my-page.md#some-section',
[
'bookSlug' => 'dev',
'languageCode' => 'en',
'versionDescriptor' => 'latest',
'editionIdentifier' => 'dev/en/latest',
'path' => 'category/foo/my-page.md',
'fragment' => 'some-section',
],
],
[
'dev/#some-section',
[
'bookSlug' => 'dev',
'languageCode' => NULL,
'versionDescriptor' => NULL,
'editionIdentifier' => NULL,
'path' => NULL,
'fragment' => 'some-section',
],
],
];
}
}
......@@ -40,4 +40,82 @@ class StringToolsTest extends \PHPUnit_Framework_TestCase
],
];
}
/**
* @param $rule
* @param $expected
* @dataProvider redirectRuleProvider
*/
public function testParseRedirectRule($rule, $expected) {
$this->assertEquals($expected, StringTools::parseRedirectRule($rule));
}
/**
* @return array
*/
public function redirectRuleProvider() {
return [
[
'foo/bar baz/bat',
[
'from' => 'foo/bar',
'to' => 'baz/bat',
]
],
[
'/foo/bar/ /baz/bat/',
[
'from' => 'foo/bar',
'to' => 'baz/bat',
]
],
[
'foo///bar baz///bat',
[
'from' => 'foo///bar',
'to' => 'baz///bat',
]
],
[
" /foo/bar /baz/bat \n",
[
'from' => 'foo/bar',
'to' => 'baz/bat',
]
],
[
'#foo/bar baz/bat',
NULL,
],
[
'foo/bar/baz/bat',
NULL,
],
[
' ',
NULL,
],
[
'foo/bar baz/bat spam/eggs',
[
'from' => 'foo/bar',
'to' => 'baz/bat',
]
],
[
NULL,
NULL,
],
];
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment