diff --git a/controller/Controller.class.php b/controller/Controller.class.php
index f070e81a..4d0d2132 100644
--- a/controller/Controller.class.php
+++ b/controller/Controller.class.php
@@ -6,8 +6,8 @@ class Controller
{
try
{
- $viewAndParams = static::execute($uri);
- $viewTemplate = $viewAndParams[0];
+ $viewAndParams = static::execute(Request::getMethod(), $uri);
+ $viewTemplate = $viewAndParams[0];
$viewParameters = $viewAndParams[1] ?? [];
if (!IS_PRODUCTION && isset($viewAndParams[2]))
{
@@ -46,96 +46,86 @@ class Controller
}
}
- public static function execute($uri)
+ public static function execute($method, $uri)
{
- switch($uri)
+ $router = static::getRouterWithRoutes();
+ try
{
- case '/':
- return ContentActions::executeHome();
- case '/get':
- case '/windows':
- case '/ios':
- case '/android':
- case '/linux':
- case '/osx':
- return DownloadActions::executeGet();
- case '/postcommit':
- return OpsActions::executePostCommit();
- case '/log-upload':
- return OpsActions::executeLogUpload();
- case '/list-subscribe':
- return MailActions::executeListSubscribe();
- case '/press-kit.zip':
- return ContentActions::executePressKit();
- case '/LBRY-deck.pdf':
- case '/deck.pdf':
- return static::redirect('https://www.dropbox.com/s/0xj4vgucsbi8rtv/lbry-deck.pdf?dl=1');
- case '/pln.pdf':
- case '/plan.pdf':
- return static::redirect('https://www.dropbox.com/s/uevjrwnyr672clj/lbry-pln.pdf?dl=1');
- case '/lbry-osx-latest.dmg':
- case '/lbry-linux-latest.deb':
- case '/dl/lbry_setup.sh':
- return static::redirect('/get', 301);
- case '/get/lbry.dmg':
- return static::redirect(DownloadActions::getDownloadUrl(DownloadActions::OS_OSX) ?: '/get');
- case '/get/lbry.deb':
- return static::redirect(DownloadActions::getDownloadUrl(DownloadActions::OS_LINUX) ?: '/get');
- case '/art':
- return static::redirect('/what', 301);
- case '/why':
- case '/feedback':
- return static::redirect('/learn', 301);
- case '/faq/when-referral-payouts':
- return static::redirect('/faq/referrals', 301);
+ $dispatcher = new Routing\Dispatcher($router->getData());
+ return $dispatcher->dispatch($method, $uri);
}
-
- $newsPattern = '#^' . ContentActions::URL_NEWS . '(/|$)#';
- if (preg_match($newsPattern, $uri))
+ catch (\Routing\HttpRouteNotFoundException $e)
{
- Response::enableHttpCache();
- $slug = preg_replace($newsPattern, '', $uri);
- if ($slug == ContentActions::RSS_SLUG)
+ return NavActions::execute404();
+ }
+ }
+
+ protected static function getRouterWithRoutes(): \Routing\RouteCollector
+ {
+ $router = new Routing\RouteCollector();
+
+ $router->get(['/', 'home'], 'ContentActions::executeHome');
+
+ $router->get(['/get', 'get'], 'DownloadActions::executeGet');
+ $router->get(['/windows', 'get-windows'], 'DownloadActions::executeGet');
+ $router->get(['/linux', 'get-linux'], 'DownloadActions::executeGet');
+ $router->get(['/osx', 'get-osx'], 'DownloadActions::executeGet');
+ $router->get(['/android', 'get-android'], 'DownloadActions::executeGet');
+ $router->get(['/ios', 'get-ios'], 'DownloadActions::executeGet');
+
+ $router->get(['/press-kit.zip', 'press-kit'], 'ContentActions::executePressKit');
+
+ $router->post('/postcommit', 'OpsActions::executePostCommit');
+ $router->post('/log-upload', 'OpsActions::executeLogUpload');
+ $router->post(['/list-subscribe', 'list-subscribe'], 'MailActions::executeListSubscribe');
+
+ $permanentRedirects = [
+ '/lbry-osx-latest.dmg' => '/get',
+ '/lbry-linux-latest.deb' => '/get',
+ '/dl/lbry_setup.sh' => '/get',
+ '/art' => '/what',
+ '/why' => '/learn',
+ '/feedback' => '/learn',
+ '/faq/when-referral-payouts' => '/faq/referrals',
+ ];
+
+ $tempRedirects = [
+ '/LBRY-deck.pdf' => 'https://www.dropbox.com/s/0xj4vgucsbi8rtv/lbry-deck.pdf?dl=1',
+ '/deck.pdf' => 'https://www.dropbox.com/s/0xj4vgucsbi8rtv/lbry-deck.pdf?dl=1',
+ '/pln.pdf' => 'https://www.dropbox.com/s/uevjrwnyr672clj/lbry-pln.pdf?dl=1',
+ '/plan.pdf' => 'https://www.dropbox.com/s/uevjrwnyr672clj/lbry-pln.pdf?dl=1',
+ '/get/lbry.dmg' => DownloadActions::getDownloadUrl(DownloadActions::OS_OSX) ?: '/get',
+ '/get/lbry.deb' => DownloadActions::getDownloadUrl(DownloadActions::OS_LINUX) ?: '/get',
+ ];
+
+ foreach ([302 => $tempRedirects, 301 => $permanentRedirects] as $code => $redirects)
+ {
+ foreach ($redirects as $src => $target)
{
- return ContentActions::executeRss();
+ $router->any($src, function () use ($target, $code) { return static::redirect($target, $code); });
}
- return $slug ? ContentActions::executeNewsPost($uri) : ContentActions::executeNews();
}
- $faqPattern = '#^' . ContentActions::URL_FAQ . '(/|$)#';
- if (preg_match($faqPattern, $uri))
- {
- Response::enableHttpCache();
- $slug = preg_replace($faqPattern, '', $uri);
- return $slug ? ContentActions::executeFaqPost($uri) : ContentActions::executeFaq();
- }
+ $router->get([ContentActions::URL_NEWS . '/{slug:c}?', 'news'], 'ContentActions::executeNews');
+ $router->get([ContentActions::URL_FAQ . '/{slug:c}?', 'faq'], 'ContentActions::executeFaq');
+ $router->get([BountyActions::URL_BOUNTY . '/{slug:c}?', 'bounty'], 'BountyActions::executeShow');
- $bountyPattern = '#^' . BountyActions::URL_BOUNTY_LIST . '(/|$)#';
- if (preg_match($bountyPattern, $uri))
- {
- Response::enableHttpCache();
- $slug = preg_replace($bountyPattern, '', $uri);
- return $slug ? BountyActions::executeShow($uri) : BountyActions::executeList($uri);
- }
+ $router->any(['/signup{whatever}?', 'signup'], 'DownloadActions::executeSignup');
- $accessPattern = '#^/signup#';
- if (preg_match($accessPattern, $uri))
+ $router->get('/{slug}', function (string $slug)
{
- return DownloadActions::executeSignup();
- }
+ if (View::exists('page/' . $slug))
+ {
+ Response::enableHttpCache();
+ return ['page/' . $slug, []];
+ }
+ else
+ {
+ return NavActions::execute404();
+ }
+ });
-
- $noSlashUri = ltrim($uri, '/');
- if (View::exists('page/' . $noSlashUri))
- {
- Response::enableHttpCache();
- return ['page/' . $noSlashUri, []];
- }
- else
- {
- Response::setStatus(404);
- return ['page/404', []];
- }
+ return $router;
}
public static function redirect($url, $statusCode = 302)
diff --git a/controller/Request.class.php b/controller/Request.class.php
index 96de0774..969e3ecd 100644
--- a/controller/Request.class.php
+++ b/controller/Request.class.php
@@ -54,7 +54,7 @@ class Request
public static function getRelativeUri(): string
{
- return $_SERVER['REQUEST_URI'] ?? '';
+ return isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '';
}
public static function isGzipAccepted(): bool
diff --git a/controller/action/BountyActions.class.php b/controller/action/BountyActions.class.php
index 8388f224..cb227709 100644
--- a/controller/action/BountyActions.class.php
+++ b/controller/action/BountyActions.class.php
@@ -2,10 +2,29 @@
class BountyActions extends Actions
{
- const URL_BOUNTY_LIST = '/bounty';
+ const
+ SLUG_BOUNTY = 'bounty',
+ URL_BOUNTY = '/' . self::SLUG_BOUNTY,
+ VIEW_FOLDER_FAQ = ROOT_DIR . '/posts/' . self::SLUG_BOUNTY;
- public static function executeList()
+ public static function executeList(string $slug = null): array
{
+ Response::enableHttpCache();
+
+ if ($slug)
+ {
+ list($metadata, $postHtml) = View::parseMarkdown(static::VIEW_FOLDER_FAQ . '/' . $slug . '.md');
+ if (!$postHtml)
+ {
+ return NavActions::execute404();
+ }
+
+ return ['bounty/show', [
+ 'postHtml' => $postHtml,
+ 'metadata' => $metadata
+ ]];
+ }
+
$allBounties = Post::find(ROOT_DIR . '/posts/bounty');
$allCategories = ['' => ''] + Post::collectMetadata($allBounties, 'category');
@@ -41,17 +60,4 @@ class BountyActions extends Actions
'selectedStatus' => $selectedStatus
]];
}
-
- public static function executeShow($relativeUri)
- {
- list($metadata, $postHtml) = View::parseMarkdown(ROOT_DIR . '/posts/' . $relativeUri . '.md');
- if (!$postHtml)
- {
- return ['page/404', []];
- }
- return ['bounty/show', [
- 'postHtml' => $postHtml,
- 'metadata' => $metadata
- ]];
- }
}
\ No newline at end of file
diff --git a/controller/action/ContentActions.class.php b/controller/action/ContentActions.class.php
index 8d8601e1..2a410a22 100644
--- a/controller/action/ContentActions.class.php
+++ b/controller/action/ContentActions.class.php
@@ -2,11 +2,16 @@
class ContentActions extends Actions
{
- const RSS_SLUG = 'rss.xml',
- URL_NEWS = '/news',
- URL_FAQ = '/faq',
- VIEW_FOLDER_NEWS = ROOT_DIR . '/posts/news',
- VIEW_FOLDER_FAQ = ROOT_DIR . '/posts/faq';
+ const
+ SLUG_RSS = 'rss.xml',
+ SLUG_NEWS = 'news',
+ SLUG_FAQ = 'faq',
+
+ URL_NEWS = '/' . self::SLUG_NEWS,
+ URL_FAQ = '/' . self::SLUG_FAQ,
+
+ VIEW_FOLDER_NEWS = ROOT_DIR . '/posts/' . self::SLUG_NEWS,
+ VIEW_FOLDER_FAQ = ROOT_DIR . '/posts/' . self::SLUG_FAQ;
public static function executeHome(): array
{
@@ -14,104 +19,107 @@ class ContentActions extends Actions
return ['page/home'];
}
- public static function executeFaq(): array
+ public static function executeNews(string $slug = null): array
{
- $allPosts = Post::find(static::VIEW_FOLDER_FAQ);
+ Response::enableHttpCache();
- $allCategories = array_merge(['' => ''] + Post::collectMetadata($allPosts, 'category'), [
- 'getstarted' => 'Getting Started',
- 'install' => 'Installing LBRY',
- 'running' => 'Running LBRY',
- 'wallet' => 'The LBRY Wallet',
- 'hosting' => 'Hosting Content',
- 'mining' => 'Mining LBC',
- 'policy' => 'Policies',
- 'developer' => 'Developers',
- 'other' => 'Other Questions',
- ]);
- $selectedCategory = static::param('category');
- $filters = array_filter([
- 'category' => $selectedCategory && isset($allCategories[$selectedCategory]) ? $selectedCategory : null,
- ]);
-
- asort($allCategories);
-
- $posts = $filters ? Post::filter($allPosts, $filters) : $allPosts ;
-
-
- $groups = array_fill_keys(array_keys($allCategories), []);
-
- foreach($posts as $post)
+ if (!$slug)
{
- $groups[$post->getCategory()][] = $post;
+ $posts = Post::find(static::VIEW_FOLDER_NEWS, Post::SORT_DATE_DESC);
+ return ['content/news', [
+ 'posts' => $posts,
+ View::LAYOUT_PARAMS => [
+ 'showRssLink' => true
+ ]
+ ]];
}
- return ['content/faq', [
- 'categories' => $allCategories,
- 'selectedCategory' => $selectedCategory,
- 'postGroups' => $groups
- ]];
- }
+ if ($slug == static::SLUG_RSS)
+ {
+ $posts = Post::find(static::VIEW_FOLDER_NEWS, Post::SORT_DATE_DESC);
+ Response::setHeader(Response::HEADER_CONTENT_TYPE, 'text/xml; charset=utf-8');
+ return ['content/rss', [
+ 'posts' => array_slice($posts, 0, 10),
+ '_no_layout' => true
+ ]];
+ }
- public static function executeNews(): array
- {
- $posts = Post::find(static::VIEW_FOLDER_NEWS, Post::SORT_DATE_DESC);
- return ['content/news', [
- 'posts' => $posts,
- View::LAYOUT_PARAMS => [
- 'showRssLink' => true
- ]
- ]];
- }
-
-
- public static function executeRss(): array
- {
- $posts = Post::find(static::VIEW_FOLDER_NEWS, Post::SORT_DATE_DESC);
- Response::setHeader(Response::HEADER_CONTENT_TYPE, 'text/xml; charset=utf-8');
- return ['content/rss', [
- 'posts' => array_slice($posts, 0, 10),
- '_no_layout' => true
- ]];
- }
-
- public static function executeNewsPost($relativeUri): array
- {
try
{
- $post = Post::load(ltrim($relativeUri, '/'));
+ $post = Post::load(static::SLUG_NEWS . '/' . ltrim($slug, '/'));
}
catch (PostNotFoundException $e)
{
- return ['page/404', []];
+ return NavActions::execute404();
}
+
return ['content/news-post', [
- 'post' => $post,
+ 'post' => $post,
View::LAYOUT_PARAMS => [
'showRssLink' => true
]
]];
}
- public static function executeFaqPost($relativeUri): array
+
+ public static function executeFaq(string $slug = null): array
{
+ Response::enableHttpCache();
+
+ if (!$slug)
+ {
+ $allPosts = Post::find(static::VIEW_FOLDER_FAQ);
+
+ $allCategories = array_merge(['' => ''] + Post::collectMetadata($allPosts, 'category'), [
+ 'getstarted' => 'Getting Started',
+ 'install' => 'Installing LBRY',
+ 'running' => 'Running LBRY',
+ 'wallet' => 'The LBRY Wallet',
+ 'hosting' => 'Hosting Content',
+ 'mining' => 'Mining LBC',
+ 'policy' => 'Policies',
+ 'developer' => 'Developers',
+ 'other' => 'Other Questions',
+ ]);
+ $selectedCategory = static::param('category');
+ $filters = array_filter([
+ 'category' => $selectedCategory && isset($allCategories[$selectedCategory]) ? $selectedCategory : null,
+ ]);
+
+ asort($allCategories);
+
+ $posts = $filters ? Post::filter($allPosts, $filters) : $allPosts;
+
+
+ $groups = array_fill_keys(array_keys($allCategories), []);
+
+ foreach ($posts as $post)
+ {
+ $groups[$post->getCategory()][] = $post;
+ }
+
+ return ['content/faq', [
+ 'categories' => $allCategories,
+ 'selectedCategory' => $selectedCategory,
+ 'postGroups' => $groups
+ ]];
+ }
+
try
{
- $post = Post::load(ltrim($relativeUri, '/'));
+ $post = Post::load(static::SLUG_FAQ . '/' . ltrim($slug, '/'));
}
catch (PostNotFoundException $e)
{
- return ['page/404', []];
+ return NavActions::execute404();
}
- return ['content/faq-post', [
- 'post' => $post,
- ]];
+ return ['content/faq-post', ['post' => $post,]];
}
public static function executePressKit(): array
{
$zipFileName = 'lbry-press-kit-' . date('Y-m-d') . '.zip';
- $zipPath = tempnam('/tmp', $zipFileName);
+ $zipPath = tempnam('/tmp', $zipFileName);
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::OVERWRITE);
@@ -133,25 +141,26 @@ class ContentActions extends Actions
//
// $zip->addFromString('press.html', $html);
- foreach(glob(ROOT_DIR . '/web/img/press/*') as $productImgPath)
+ foreach (glob(ROOT_DIR . '/web/img/press/*') as $productImgPath)
{
$imgPathTokens = explode('/', $productImgPath);
- $imgName = $imgPathTokens[count($imgPathTokens) - 1];
+ $imgName = $imgPathTokens[count($imgPathTokens) - 1];
$zip->addFile($productImgPath, '/logo_and_product/' . $imgName);
}
- foreach(glob(ROOT_DIR . '/posts/bio/*.md') as $bioPath)
+ foreach (glob(ROOT_DIR . '/posts/bio/*.md') as $bioPath)
{
list($metadata, $bioHtml) = View::parseMarkdown($bioPath);
$zip->addFile($bioPath, '/team_bios/' . $metadata['name'] . ' - ' . $metadata['role'] . '.txt');
}
- foreach(array_filter(glob(ROOT_DIR . '/web/img/team/*.jpg'), function($path) {
+ foreach (array_filter(glob(ROOT_DIR . '/web/img/team/*.jpg'), function ($path)
+ {
return strpos($path, 'spooner') === false;
}) as $bioImgPath)
{
$imgPathTokens = explode('/', $bioImgPath);
- $imgName = str_replace('644x450', 'lbry', $imgPathTokens[count($imgPathTokens) - 1]);
+ $imgName = str_replace('644x450', 'lbry', $imgPathTokens[count($imgPathTokens) - 1]);
$zip->addFile($bioImgPath, '/team_photos/' . $imgName);
}
@@ -163,20 +172,20 @@ class ContentActions extends Actions
return ['internal/zip', [
'_no_layout' => true,
- 'zipPath' => $zipPath
+ 'zipPath' => $zipPath
]];
}
public static function prepareBioPartial(array $vars): array
{
$person = $vars['person'];
- $path = 'bio/' . $person . '.md';
+ $path = 'bio/' . $person . '.md';
list($metadata, $bioHtml) = View::parseMarkdown($path);
$relativeImgSrc = '/img/team/' . $person . '-644x450.jpg';
- $imgSrc = file_exists(ROOT_DIR . '/web' . $relativeImgSrc) ? $relativeImgSrc : '/img/team/spooner-644x450.jpg';
+ $imgSrc = file_exists(ROOT_DIR . '/web' . $relativeImgSrc) ? $relativeImgSrc : '/img/team/spooner-644x450.jpg';
return $vars + $metadata + [
- 'imgSrc' => $imgSrc,
- 'bioHtml' => $bioHtml,
+ 'imgSrc' => $imgSrc,
+ 'bioHtml' => $bioHtml,
'orientation' => 'vertical'
];
}
@@ -185,8 +194,8 @@ class ContentActions extends Actions
{
$post = $vars['post'];
return [
- 'authorName' => $post->getAuthorName(),
- 'photoImgSrc' => $post->getAuthorPhoto(),
+ 'authorName' => $post->getAuthorName(),
+ 'photoImgSrc' => $post->getAuthorPhoto(),
'authorBioHtml' => $post->getAuthorBioHtml()
];
}
diff --git a/controller/action/DownloadActions.class.php b/controller/action/DownloadActions.class.php
index abe5be3f..025ef910 100644
--- a/controller/action/DownloadActions.class.php
+++ b/controller/action/DownloadActions.class.php
@@ -49,7 +49,7 @@ class DownloadActions extends Actions
if (!Session::get(Session::KEY_DOWNLOAD_ALLOWED))
{
- return ['download/get', ['os' => static::guessOs()]];
+ return ['download/get'];
}
$osChoices = static::getOses();
@@ -169,7 +169,7 @@ class DownloadActions extends Actions
protected static function guessOs()
{
//if exact OS is requested, use that
- $uri = strtok(Request::getRelativeUri(), '?');
+ $uri = Request::getRelativeUri();
foreach (static::getOses() as $os => $osChoice)
{
if ($osChoice[0] == $uri)
diff --git a/controller/action/NavActions.class.php b/controller/action/NavActions.class.php
index 5528bb8e..6de028e2 100644
--- a/controller/action/NavActions.class.php
+++ b/controller/action/NavActions.class.php
@@ -1,10 +1,5 @@
true
];
}
+
+ public static function execute404()
+ {
+ Response::setStatus(404);
+ return ['page/404'];
+ }
}
\ No newline at end of file
diff --git a/data/i18n/en.yaml b/data/i18n/en.yaml
index c17ede01..84feb73a 100644
--- a/data/i18n/en.yaml
+++ b/data/i18n/en.yaml
@@ -72,8 +72,7 @@ learn:
100: LBRY in 100 Seconds
art: Art in the Internet Age
essay: Read the Essay
- exchange: Bittrex Exchange
- exchange2: Poloniex Exchange
+ exchange_faq: Buy/Sell LBRY Credits
explore: Explore
explorer: Block Explorer
how: Learn how LBRY will forever improve how we create and share with one another.
diff --git a/lib/routing/BadRouteException.class.php b/lib/routing/BadRouteException.class.php
new file mode 100644
index 00000000..33d07f1e
--- /dev/null
+++ b/lib/routing/BadRouteException.class.php
@@ -0,0 +1,5 @@
+staticRouteMap = $data->getStaticRoutes();
+
+ $this->variableRouteData = $data->getVariableRoutes();
+
+ $this->filters = $data->getFilters();
+
+ if ($resolver === null)
+ {
+ $this->handlerResolver = new HandlerResolver();
+ }
+ else
+ {
+ $this->handlerResolver = $resolver;
+ }
+ }
+
+ /**
+ * Dispatch a route for the given HTTP Method / URI.
+ *
+ * @param $httpMethod
+ * @param $uri
+ *
+ * @return mixed|null
+ */
+ public function dispatch($httpMethod, $uri)
+ {
+ list($handler, $filters, $vars) = $this->dispatchRoute($httpMethod, trim($uri, '/'));
+
+ list($beforeFilter, $afterFilter) = $this->parseFilters($filters);
+
+ if (($response = $this->dispatchFilters($beforeFilter)) !== null)
+ {
+ return $response;
+ }
+
+ $resolvedHandler = $this->handlerResolver->resolve($handler);
+
+ $response = call_user_func_array($resolvedHandler, $vars);
+
+ return $this->dispatchFilters($afterFilter, $response);
+ }
+
+ /**
+ * Dispatch a route filter.
+ *
+ * @param $filters
+ * @param null $response
+ *
+ * @return mixed|null
+ */
+ private function dispatchFilters($filters, $response = null)
+ {
+ while ($filter = array_shift($filters))
+ {
+ $handler = $this->handlerResolver->resolve($filter);
+
+ if (($filteredResponse = call_user_func($handler, $response)) !== null)
+ {
+ return $filteredResponse;
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Normalise the array filters attached to the route and merge with any global filters.
+ *
+ * @param $filters
+ *
+ * @return array
+ */
+ private function parseFilters($filters)
+ {
+ $beforeFilter = [];
+ $afterFilter = [];
+
+ if (isset($filters[Route::BEFORE]))
+ {
+ $beforeFilter = array_intersect_key($this->filters, array_flip((array)$filters[Route::BEFORE]));
+ }
+
+ if (isset($filters[Route::AFTER]))
+ {
+ $afterFilter = array_intersect_key($this->filters, array_flip((array)$filters[Route::AFTER]));
+ }
+
+ return [$beforeFilter, $afterFilter];
+ }
+
+ /**
+ * Perform the route dispatching. Check static routes first followed by variable routes.
+ *
+ * @param $httpMethod
+ * @param $uri
+ *
+ * @throws Exception\HttpRouteNotFoundException
+ */
+ private function dispatchRoute($httpMethod, $uri)
+ {
+ if (isset($this->staticRouteMap[$uri]))
+ {
+ return $this->dispatchStaticRoute($httpMethod, $uri);
+ }
+
+ return $this->dispatchVariableRoute($httpMethod, $uri);
+ }
+
+ /**
+ * Handle the dispatching of static routes.
+ *
+ * @param $httpMethod
+ * @param $uri
+ *
+ * @return mixed
+ * @throws Exception\HttpMethodNotAllowedException
+ */
+ private function dispatchStaticRoute($httpMethod, $uri)
+ {
+ $routes = $this->staticRouteMap[$uri];
+
+ if (!isset($routes[$httpMethod]))
+ {
+ $httpMethod = $this->checkFallbacks($routes, $httpMethod);
+ }
+
+ return $routes[$httpMethod];
+ }
+
+ /**
+ * Check fallback routes: HEAD for GET requests followed by the ANY attachment.
+ *
+ * @param $routes
+ * @param $httpMethod
+ *
+ * @throws Exception\HttpMethodNotAllowedException
+ */
+ private function checkFallbacks($routes, $httpMethod)
+ {
+ $additional = [Route::ANY];
+
+ if ($httpMethod === Route::HEAD)
+ {
+ $additional[] = Route::GET;
+ }
+
+ foreach ($additional as $method)
+ {
+ if (isset($routes[$method]))
+ {
+ return $method;
+ }
+ }
+
+ $this->matchedRoute = $routes;
+
+ throw new HttpMethodNotAllowedException('Allow: ' . implode(', ', array_keys($routes)));
+ }
+
+ /**
+ * Handle the dispatching of variable routes.
+ *
+ * @param $httpMethod
+ * @param $uri
+ *
+ * @throws Exception\HttpMethodNotAllowedException
+ * @throws Exception\HttpRouteNotFoundException
+ */
+ private function dispatchVariableRoute($httpMethod, $uri)
+ {
+ foreach ($this->variableRouteData as $data)
+ {
+ if (!preg_match($data['regex'], $uri, $matches))
+ {
+ continue;
+ }
+
+ $count = count($matches);
+
+ while (!isset($data['routeMap'][$count++]))
+ {
+ ;
+ }
+
+ $routes = $data['routeMap'][$count - 1];
+
+ if (!isset($routes[$httpMethod]))
+ {
+ $httpMethod = $this->checkFallbacks($routes, $httpMethod);
+ }
+
+ foreach (array_values($routes[$httpMethod][2]) as $i => $varName)
+ {
+ if (!isset($matches[$i + 1]) || $matches[$i + 1] === '')
+ {
+ unset($routes[$httpMethod][2][$varName]);
+ }
+ else
+ {
+ $routes[$httpMethod][2][$varName] = $matches[$i + 1];
+ }
+ }
+
+ return $routes[$httpMethod];
+ }
+
+ throw new HttpRouteNotFoundException('Route ' . $uri . ' does not exist');
+ }
+}
diff --git a/lib/routing/HandlerResolver.class.php b/lib/routing/HandlerResolver.class.php
new file mode 100644
index 00000000..9918d2f7
--- /dev/null
+++ b/lib/routing/HandlerResolver.class.php
@@ -0,0 +1,16 @@
+routeParser = $routeParser ?: new RouteParser();
+ }
+
+ public function hasRoute(string $name): bool
+ {
+ return isset($this->reverse[$name]);
+ }
+
+ public function route(string $name, array $args = null): string
+ {
+ $url = [];
+
+ $replacements = is_null($args) ? [] : array_values($args);
+
+ $variable = 0;
+
+ foreach ($this->reverse[$name] as $part)
+ {
+ if (!$part['variable'])
+ {
+ $url[] = $part['value'];
+ }
+ elseif (isset($replacements[$variable]))
+ {
+ if ($part['optional'])
+ {
+ $url[] = '/';
+ }
+
+ $url[] = $replacements[$variable++];
+ }
+ elseif (!$part['optional'])
+ {
+ throw new BadRouteException("Expecting route variable '{$part['name']}'");
+ }
+ }
+
+ return implode('', $url);
+ }
+
+ public function addRoute(string $httpMethod, $route, $handler, array $filters = []): RouteCollector
+ {
+
+ if (is_array($route))
+ {
+ list($route, $name) = $route;
+ }
+
+ $route = $this->addPrefix($this->trim($route));
+
+ list($routeData, $reverseData) = $this->routeParser->parse($route);
+
+ if (isset($name))
+ {
+ $this->reverse[$name] = $reverseData;
+ }
+
+ $filters = array_merge_recursive($this->globalFilters, $filters);
+
+ isset($routeData[1]) ?
+ $this->addVariableRoute($httpMethod, $routeData, $handler, $filters) :
+ $this->addStaticRoute($httpMethod, $routeData, $handler, $filters);
+
+ return $this;
+ }
+
+ private function addStaticRoute(string $httpMethod, $routeData, $handler, $filters)
+ {
+ $routeStr = $routeData[0];
+
+ if (isset($this->staticRoutes[$routeStr][$httpMethod]))
+ {
+ throw new BadRouteException("Cannot register two routes matching '$routeStr' for method '$httpMethod'");
+ }
+
+ foreach ($this->regexToRoutesMap as $regex => $routes)
+ {
+ if (isset($routes[$httpMethod]) && preg_match('~^' . $regex . '$~', $routeStr))
+ {
+ throw new BadRouteException("Static route '$routeStr' is shadowed by previously defined variable route '$regex' for method '$httpMethod'");
+ }
+ }
+
+ $this->staticRoutes[$routeStr][$httpMethod] = [$handler, $filters, []];
+ }
+
+ private function addVariableRoute(string $httpMethod, $routeData, $handler, $filters)
+ {
+ list($regex, $variables) = $routeData;
+
+ if (isset($this->regexToRoutesMap[$regex][$httpMethod]))
+ {
+ throw new BadRouteException("Cannot register two routes matching '$regex' for method '$httpMethod'");
+ }
+
+ $this->regexToRoutesMap[$regex][$httpMethod] = [$handler, $filters, $variables];
+ }
+
+ public function group(array $filters, \Closure $callback)
+ {
+ $oldGlobalFilters = $this->globalFilters;
+
+ $oldGlobalPrefix = $this->globalRoutePrefix;
+
+ $this->globalFilters =
+ array_merge_recursive($this->globalFilters, array_intersect_key($filters, [Route::AFTER => 1, Route::BEFORE => 1]));
+
+ $newPrefix = isset($filters[Route::PREFIX]) ? $this->trim($filters[Route::PREFIX]) : null;
+
+ $this->globalRoutePrefix = $this->addPrefix($newPrefix);
+
+ $callback($this);
+
+ $this->globalFilters = $oldGlobalFilters;
+
+ $this->globalRoutePrefix = $oldGlobalPrefix;
+ }
+
+ private function addPrefix(string $route)
+ {
+ return $this->trim($this->trim($this->globalRoutePrefix) . '/' . $route);
+ }
+
+ public function filter(string $name, $handler)
+ {
+ $this->filters[$name] = $handler;
+ }
+
+
+ public function get($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::GET, $route, $handler, $filters);
+ }
+
+ public function head($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::HEAD, $route, $handler, $filters);
+ }
+
+ public function post($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::POST, $route, $handler, $filters);
+ }
+
+ public function put($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::PUT, $route, $handler, $filters);
+ }
+
+ public function patch($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::PATCH, $route, $handler, $filters);
+ }
+
+ public function delete($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::DELETE, $route, $handler, $filters);
+ }
+
+ public function options($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::OPTIONS, $route, $handler, $filters);
+ }
+
+ public function any($route, $handler, array $filters = [])
+ {
+ return $this->addRoute(Route::ANY, $route, $handler, $filters);
+ }
+
+
+ public function controller(string $route, string $classname, array $filters = []): RouteCollector
+ {
+ $reflection = new ReflectionClass($classname);
+
+ $validMethods = $this->getValidMethods();
+
+ $sep = $route === '/' ? '' : '/';
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
+ {
+ foreach ($validMethods as $valid)
+ {
+ if (stripos($method->name, $valid) === 0)
+ {
+ $methodName = $this->camelCaseToDashed(substr($method->name, strlen($valid)));
+
+ $params = $this->buildControllerParameters($method);
+
+ if ($methodName === self::DEFAULT_CONTROLLER_ROUTE)
+ {
+ $this->addRoute($valid, $route . $params, [$classname, $method->name], $filters);
+ }
+
+ $this->addRoute($valid, $route . $sep . $methodName . $params, [$classname, $method->name], $filters);
+
+ break;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ private function buildControllerParameters(ReflectionMethod $method): string
+ {
+ $params = '';
+
+ foreach ($method->getParameters() as $param)
+ {
+ $params .= "/{" . $param->getName() . "}" . ($param->isOptional() ? '?' : '');
+ }
+
+ return $params;
+ }
+
+ private function camelCaseToDashed(string $string): string
+ {
+ return strtolower(preg_replace('/([A-Z])/', '-$1', lcfirst($string)));
+ }
+
+ public function getValidMethods(): array
+ {
+ return [
+ Route::ANY,
+ Route::GET,
+ Route::POST,
+ Route::PUT,
+ Route::PATCH,
+ Route::DELETE,
+ Route::HEAD,
+ Route::OPTIONS,
+ ];
+ }
+
+ public function getData(): RouteData
+ {
+ return new RouteData($this->staticRoutes, $this->regexToRoutesMap ? $this->generateVariableRouteData() : [], $this->filters);
+ }
+
+ private function trim(string $route): string
+ {
+ return trim($route, '/');
+ }
+
+ private function generateVariableRouteData(): array
+ {
+ $chunkSize = $this->computeChunkSize(count($this->regexToRoutesMap));
+ $chunks = array_chunk($this->regexToRoutesMap, $chunkSize, true);
+ return array_map([$this, 'processChunk'], $chunks);
+ }
+
+ private function computeChunkSize(int $count): float
+ {
+ $numParts = max(1, round($count / self::APPROX_CHUNK_SIZE));
+ return ceil($count / $numParts);
+ }
+
+ private function processChunk($regexToRoutesMap): array
+ {
+ $routeMap = [];
+ $regexes = [];
+ $numGroups = 0;
+ foreach ($regexToRoutesMap as $regex => $routes)
+ {
+ $firstRoute = reset($routes);
+ $numVariables = count($firstRoute[2]);
+ $numGroups = max($numGroups, $numVariables);
+
+ $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
+
+ foreach ($routes as $httpMethod => $route)
+ {
+ $routeMap[$numGroups + 1][$httpMethod] = $route;
+ }
+
+ $numGroups++;
+ }
+
+ $regex = '~^(?|' . implode('|', $regexes) . ')$~';
+ return ['regex' => $regex, 'routeMap' => $routeMap];
+ }
+}
diff --git a/lib/routing/RouteData.class.php b/lib/routing/RouteData.class.php
new file mode 100644
index 00000000..1a53709f
--- /dev/null
+++ b/lib/routing/RouteData.class.php
@@ -0,0 +1,59 @@
+staticRoutes = $staticRoutes;
+
+ $this->variableRoutes = $variableRoutes;
+
+ $this->filters = $filters;
+ }
+
+ /**
+ * @return array
+ */
+ public function getStaticRoutes()
+ {
+ return $this->staticRoutes;
+ }
+
+ /**
+ * @return array
+ */
+ public function getVariableRoutes()
+ {
+ return $this->variableRoutes;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+}
diff --git a/lib/routing/RouteParser.class.php b/lib/routing/RouteParser.class.php
new file mode 100644
index 00000000..c6d9ad17
--- /dev/null
+++ b/lib/routing/RouteParser.class.php
@@ -0,0 +1,214 @@
+ ':[0-9]+}',
+ ':a}' => ':[0-9A-Za-z]+}',
+ ':h}' => ':[0-9A-Fa-f]+}',
+ ':c}' => ':[a-zA-Z0-9+_\-\.]+}'
+ ];
+
+ /**
+ * Parse a route returning the correct data format to pass to the dispatch engine.
+ *
+ * @param $route
+ *
+ * @return array
+ */
+ public function parse($route)
+ {
+ $this->reset();
+
+ $route = strtr($route, $this->regexShortcuts);
+
+ if (!$matches = $this->extractVariableRouteParts($route))
+ {
+ $reverse = [
+ 'variable' => false,
+ 'value' => $route
+ ];
+
+ return [[$route], [$reverse]];
+ }
+
+ foreach ($matches as $set)
+ {
+
+ $this->staticParts($route, $set[0][1]);
+
+ $this->validateVariable($set[1][0]);
+
+ $regexPart = (isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX);
+
+ $this->regexOffset = $set[0][1] + strlen($set[0][0]);
+
+ $match = '(' . $regexPart . ')';
+
+ $isOptional = substr($set[0][0], -1) === '?';
+
+ if ($isOptional)
+ {
+ $match = $this->makeOptional($match);
+ }
+
+ $this->reverseParts[$this->partsCounter] = [
+ 'variable' => true,
+ 'optional' => $isOptional,
+ 'name' => $set[1][0]
+ ];
+
+ $this->parts[$this->partsCounter++] = $match;
+ }
+
+ $this->staticParts($route, strlen($route));
+
+ return [[implode('', $this->parts), $this->variables], array_values($this->reverseParts)];
+ }
+
+ /**
+ * Reset the parser ready for the next route.
+ */
+ private function reset()
+ {
+ $this->parts = [];
+
+ $this->reverseParts = [];
+
+ $this->partsCounter = 0;
+
+ $this->variables = [];
+
+ $this->regexOffset = 0;
+ }
+
+ /**
+ * Return any variable route portions from the given route.
+ *
+ * @param $route
+ *
+ * @return mixed
+ */
+ private function extractVariableRouteParts($route)
+ {
+ if (preg_match_all(self::VARIABLE_REGEX, $route, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER))
+ {
+ return $matches;
+ }
+ }
+
+ /**
+ * @param $route
+ * @param $nextOffset
+ */
+ private function staticParts($route, $nextOffset)
+ {
+ $static = preg_split('~(/)~u', substr($route, $this->regexOffset, $nextOffset - $this->regexOffset), 0, PREG_SPLIT_DELIM_CAPTURE);
+
+ foreach ($static as $staticPart)
+ {
+ if ($staticPart)
+ {
+ $quotedPart = $this->quote($staticPart);
+
+ $this->parts[$this->partsCounter] = $quotedPart;
+
+ $this->reverseParts[$this->partsCounter] = [
+ 'variable' => false,
+ 'value' => $staticPart
+ ];
+
+ $this->partsCounter++;
+ }
+ }
+ }
+
+ /**
+ * @param $varName
+ */
+ private function validateVariable($varName)
+ {
+ if (isset($this->variables[$varName]))
+ {
+ throw new BadRouteException("Cannot use the same placeholder '$varName' twice");
+ }
+
+ $this->variables[$varName] = $varName;
+ }
+
+ /**
+ * @param $match
+ *
+ * @return string
+ */
+ private function makeOptional($match)
+ {
+ $previous = $this->partsCounter - 1;
+
+ if (isset($this->parts[$previous]) && $this->parts[$previous] === '/')
+ {
+ $this->partsCounter--;
+ $match = '(?:/' . $match . ')';
+ }
+
+ return $match . '?';
+ }
+
+ /**
+ * @param $part
+ *
+ * @return string
+ */
+ private function quote($part)
+ {
+ return preg_quote($part, '~');
+ }
+}
diff --git a/posts/faq/exchanges.md b/posts/faq/exchanges.md
index 7c0a2f0c..0bd3eb5d 100644
--- a/posts/faq/exchanges.md
+++ b/posts/faq/exchanges.md
@@ -7,3 +7,6 @@ We are listed on several exchanges. You can buy or sell credits at one of these:
- [Bittrex](https://bittrex.com/Market/Index?MarketName=BTC-LBC)
- [Poloniex](https://poloniex.com/exchange#btc_lbc)
+- [Shapeshift](https://shapeshift.io)
+- [Changelly](https://changelly.com/exchange/BTC/LBC/1)
+- [BitSquare](https://bitsquare.io/)
\ No newline at end of file
diff --git a/view/template/content/rss.php b/view/template/content/rss.php
index 076b3f17..5deb064b 100644
--- a/view/template/content/rss.php
+++ b/view/template/content/rss.php
@@ -7,7 +7,7 @@
https://github.com/lbryio/lbry.io
{{rss.lang}}
Sat, 07 Sep 2002 09:42:31 GMT ?>
-
+
-
getTitle()) ?>
diff --git a/view/template/download/get.php b/view/template/download/get.php
index 93a700e3..f0a91254 100644
--- a/view/template/download/get.php
+++ b/view/template/download/get.php
@@ -1,4 +1,4 @@
-
+
false]) ?>
diff --git a/view/template/layout/basic.php b/view/template/layout/basic.php
index 0bb5925b..ce145081 100644
--- a/view/template/layout/basic.php
+++ b/view/template/layout/basic.php
@@ -26,7 +26,7 @@
-
+
diff --git a/view/template/page/learn.php b/view/template/page/learn.php
index 5bf08571..10726f6c 100644
--- a/view/template/page/learn.php
+++ b/view/template/page/learn.php
@@ -30,10 +30,7 @@
{{learn.explorer}}
-