This commit is contained in:
Alex Grintsvayg 2016-09-05 18:19:12 -04:00
parent a05ac0bfa9
commit 0bc617a0b9
23 changed files with 1100 additions and 196 deletions

View file

@ -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)

View file

@ -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

View file

@ -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
]];
}
}

View file

@ -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()
];
}

View file

@ -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)

View file

@ -1,10 +1,5 @@
<?php
/**
* Description of NavActions
*
* @author jeremy
*/
class NavActions extends Actions
{
protected static $navUri;
@ -39,4 +34,10 @@ class NavActions extends Actions
'isDark' => true
];
}
public static function execute404()
{
Response::setStatus(404);
return ['page/404'];
}
}

View file

@ -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.

View file

@ -0,0 +1,5 @@
<?php
namespace Routing;
class RouterBadRouteException extends \LogicException {}

View file

@ -0,0 +1,230 @@
<?php
namespace Routing;
class Dispatcher
{
private $staticRouteMap;
private $variableRouteData;
private $filters;
private $handlerResolver;
public $matchedRoute;
/**
* Create a new route dispatcher.
*
* @param RouteDataInterface $data
* @param HandlerResolverInterface $resolver
*/
public function __construct(RouteData $data, HandlerResolverInterface $resolver = null)
{
$this->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');
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Routing;
class HandlerResolver implements HandlerResolverInterface
{
public function resolve($handler)
{
if (is_array($handler) && is_string($handler[0]))
{
$handler[0] = new $handler[0];
}
return $handler;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Routing;
interface HandlerResolverInterface
{
public function resolve($handler);
}

View file

@ -0,0 +1,5 @@
<?php
namespace Routing;
class HttpException extends \Exception {}

View file

@ -0,0 +1,5 @@
<?php
namespace Routing;
class HttpMethodNotAllowedException extends HttpException {}

View file

@ -0,0 +1,6 @@
<?php
namespace Routing;
class HttpRouteNotFoundException extends HttpException {}

View file

@ -0,0 +1,26 @@
<?php
namespace Routing;
class Route
{
/**
* Constants for before and after filters
*/
const BEFORE = 'before';
const AFTER = 'after';
const PREFIX = 'prefix';
/**
* Constants for common HTTP methods
*/
const ANY = 'ANY';
const GET = 'GET';
const HEAD = 'HEAD';
const POST = 'POST';
const PUT = 'PUT';
const PATCH = 'PATCH';
const DELETE = 'DELETE';
const OPTIONS = 'OPTIONS';
}

View file

@ -0,0 +1,325 @@
<?php
namespace Routing;
class RouteCollector
{
const DEFAULT_CONTROLLER_ROUTE = 'index';
const APPROX_CHUNK_SIZE = 10;
/**
* @var RouteParser
*/
private $routeParser;
/**
* @var array
*/
private $filters = [];
/**
* @var array
*/
private $staticRoutes = [];
/**
* @var array
*/
private $regexToRoutesMap = [];
/**
* @var array
*/
private $reverse = [];
/**
* @var array
*/
private $globalFilters = [];
/**
* @var string
*/
private $globalRoutePrefix = '';
public function __construct(RouteParser $routeParser = null)
{
$this->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];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Routing;
class RouteData
{
/**
* @var array
*/
private $variableRoutes;
/**
* @var array
*/
private $staticRoutes;
/**
* @var array
*/
private $filters;
/**
* @param array $staticRoutes
* @param array $variableRoutes
* @param array $filters
*/
public function __construct(array $staticRoutes, array $variableRoutes, array $filters)
{
$this->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;
}
}

View file

@ -0,0 +1,214 @@
<?php
namespace Routing;
/**
* Parses routes of the following form:
*
* "/user/{name}/{id:[0-9]+}?"
*/
class RouteParser
{
/**
* Search through the given route looking for dynamic portions.
*
* Using ~ as the regex delimiter.
*
* We start by looking for a literal '{' character followed by any amount of whitespace.
* The next portion inside the parentheses looks for a parameter name containing alphanumeric characters or underscore.
*
* After this we look for the ':\d+' and ':[0-9]+' style portion ending with a closing '}' character.
*
* Finally we look for an optional '?' which is used to signify an optional route.
*/
const VARIABLE_REGEX =
"~\{
\s* ([a-zA-Z0-9_]*) \s*
(?:
: \s* ([^{]+(?:\{.*?\})?)
)?
\}\??~x";
/**
* The default parameter character restriction (One or more characters that is not a '/').
*/
const DEFAULT_DISPATCH_REGEX = '[^/]+';
private $parts;
private $reverseParts;
private $partsCounter;
private $variables;
private $regexOffset;
/**
* Handy parameter type restrictions.
*
* @var array
*/
private $regexShortcuts = [
':i}' => ':[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, '~');
}
}

View file

@ -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/)

View file

@ -7,7 +7,7 @@
<generator>https://github.com/lbryio/lbry.io</generator>
<language>{{rss.lang}}</language>
<?php //<lastBuildDate>Sat, 07 Sep 2002 09:42:31 GMT</lastBuildDate> ?>
<atom:link href="https://lbry.io<?php echo ContentActions::URL_NEWS . '/' . ContentActions::RSS_SLUG ?>" rel="self" type="application/rss+xml" />
<atom:link href="https://lbry.io<?php echo ContentActions::URL_NEWS . '/' . ContentActions::SLUG_RSS ?>" rel="self" type="application/rss+xml" />
<?php foreach ($posts as $post): ?>
<item>
<title><?php echo htmlspecialchars($post->getTitle()) ?></title>

View file

@ -1,4 +1,4 @@
<?php Response::setMetaDescription(__('description.get')) ?>
<?php Resp nse::setMetaDescription(__('description.get')) ?>
<?php NavActions::setNavUri('/get') ?>
<?php echo View::render('nav/_header', ['isDark' => false]) ?>

View file

@ -26,7 +26,7 @@
<link rel="icon" type="image/png" href="/img/fav/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/img/fav/manifest.json">
<?php if(isset($showRssLink) && $showRssLink): ?>
<link rel="alternate" type="application/rss+xml" title="LBRY News" href="<?php echo ContentActions::URL_NEWS . '/' . ContentActions::RSS_SLUG ?>" />
<link rel="alternate" type="application/rss+xml" title="LBRY News" href="<?php echo ContentActions::URL_NEWS . '/' . ContentActions::SLUG_RSS ?>" />
<?php endif ?>
<meta name="description" content="<?php echo Response::getMetaDescription() ?>">

View file

@ -30,10 +30,7 @@
<a href="http://explorer.lbry.io" class="link-primary">{{learn.explorer}}</a>
</div>
<div class="spacer1">
<a href="https://bittrex.com/Market/Index?MarketName=BTC-LBC" class="link-primary">{{learn.exchange}}</a>
</div>
<div class="spacer1">
<a href="https://poloniex.com/exchange#btc_lbc" class="link-primary">{{learn.exchange2}}</a>
<a href="/faq/exchanges" class="link-primary">{{learn.exchange_faq}}</a>
</div>
</div>
<div class="span6">