add response class, caching

This commit is contained in:
Alex Grintsvayg 2016-09-01 18:48:05 -04:00
parent 934c0d92ef
commit 77480f029f
16 changed files with 354 additions and 137 deletions

View file

@ -34,6 +34,6 @@ class Actions
'okhttp', 'python'
];
return preg_match('/(' . join('|', $bots) . ')/i', $_SERVER['HTTP_USER_AGENT']);
return preg_match('/(' . join('|', $bots) . ')/i', Request::getUserAgent());
}
}

View file

@ -2,8 +2,6 @@
class Controller
{
const HEADER_STATUS = 'Status';
public static function dispatch($uri)
{
try
@ -11,29 +9,19 @@ class Controller
$viewAndParams = static::execute($uri);
$viewTemplate = $viewAndParams[0];
$viewParameters = isset($viewAndParams[1]) ? $viewAndParams[1] : [];
$headers = isset($viewAndParams[2]) ? $viewAndParams[2] : [];
$defaultHeaders = [
'Content-Security-Policy' => "frame-ancestors 'none'",
'X-Frame-Options' => 'DENY',
'X-XSS-Protection'=> '1',
];
if (IS_PRODUCTION)
if (!IS_PRODUCTION && isset($viewAndParams[2]))
{
$defaultHeaders['Strict-Transport-Security'] = 'max-age=31536000';
throw new Exception('use response::setheader instead of returning headers');
}
static::sendHeaders(array_merge($defaultHeaders, $headers));
if ($viewTemplate === null)
{
return '';
return;
}
if (!$viewTemplate)
{
throw new LogicException('All execute methods must return a template.');
throw new LogicException('All execute methods must return a template or NULL.');
}
$layout = !(isset($viewParameters['_no_layout']) && $viewParameters['_no_layout']);
@ -44,7 +32,9 @@ class Controller
$content = View::render($viewTemplate, $viewParameters + ['fullPage' => true]);
echo $layout ? View::render('layout/basic', ['content' => $content] + $layoutParams) : $content;
Response::setContent($layout ? View::render('layout/basic', ['content' => $content] + $layoutParams) : $content);
Response::setDefaultSecurityHeaders();
Response::send();
}
catch (StopException $e)
{
@ -99,6 +89,7 @@ class Controller
$newsPattern = '#^' . ContentActions::URL_NEWS . '(/|$)#';
if (preg_match($newsPattern, $uri))
{
Response::enableHttpCache(180);
$slug = preg_replace($newsPattern, '', $uri);
if ($slug == ContentActions::RSS_SLUG)
{
@ -110,6 +101,7 @@ class Controller
$faqPattern = '#^' . ContentActions::URL_FAQ . '(/|$)#';
if (preg_match($faqPattern, $uri))
{
Response::enableHttpCache(180);
$slug = preg_replace($faqPattern, '', $uri);
return $slug ? ContentActions::executeFaqPost($uri) : ContentActions::executeFaq();
}
@ -117,6 +109,7 @@ class Controller
$bountyPattern = '#^' . BountyActions::URL_BOUNTY_LIST . '(/|$)#';
if (preg_match($bountyPattern, $uri))
{
Response::enableHttpCache(180);
$slug = preg_replace($bountyPattern, '', $uri);
return $slug ? BountyActions::executeShow($uri) : BountyActions::executeList($uri);
}
@ -131,11 +124,13 @@ class Controller
$noSlashUri = ltrim($uri, '/');
if (View::exists('page/' . $noSlashUri))
{
Response::enableHttpCache(180);
return ['page/' . $noSlashUri, []];
}
else
{
return ['page/404', [], [static::HEADER_STATUS => 404]];
Response::setStatus(404);
return ['page/404', []];
}
}
@ -148,88 +143,13 @@ class Controller
$url = str_replace('&', '&', $url);
$headers = [static::HEADER_STATUS => $statusCode];
Response::setStatus($statusCode);
if ($statusCode == 201 || ($statusCode >= 300 && $statusCode < 400))
{
$headers['Location'] = $url;
Response::setHeader(Response::HEADER_LOCATION, $url);
}
return ['internal/redirect', ['url' => $url], $headers];
}
protected static function sendHeaders(array $headers)
{
if (isset($headers[static::HEADER_STATUS]))
{
$status = 'HTTP/1.0 ' . $headers[static::HEADER_STATUS] . ' ' . static::getStatusTextForCode($headers[static::HEADER_STATUS]);
header($status);
if (substr(php_sapi_name(), 0, 3) == 'cgi')
{
// fastcgi servers cannot send this status information because it was sent by them already due to the HTT/1.0 line
// so we can safely unset them. see ticket #3191
unset($headers[static::HEADER_STATUS]);
}
}
foreach($headers as $name => $value)
{
header($name . ': ' . $value);
}
}
public static function getStatusTextForCode($code)
{
$statusTexts = [
'100' => 'Continue',
'101' => 'Switching Protocols',
'200' => 'OK',
'201' => 'Created',
'202' => 'Accepted',
'203' => 'Non-Authoritative Information',
'204' => 'No Content',
'205' => 'Reset Content',
'206' => 'Partial Content',
'300' => 'Multiple Choices',
'301' => 'Moved Permanently',
'302' => 'Found',
'303' => 'See Other',
'304' => 'Not Modified',
'305' => 'Use Proxy',
'306' => '(Unused)',
'307' => 'Temporary Redirect',
'400' => 'Bad Request',
'401' => 'Unauthorized',
'402' => 'Payment Required',
'403' => 'Forbidden',
'404' => 'Not Found',
'405' => 'Method Not Allowed',
'406' => 'Not Acceptable',
'407' => 'Proxy Authentication Required',
'408' => 'Request Timeout',
'409' => 'Conflict',
'410' => 'Gone',
'411' => 'Length Required',
'412' => 'Precondition Failed',
'413' => 'Request Entity Too Large',
'414' => 'Request-URI Too Long',
'415' => 'Unsupported Media Type',
'416' => 'Requested Range Not Satisfiable',
'417' => 'Expectation Failed',
'419' => 'Authentication Timeout',
'422' => 'Unprocessable Entity',
'426' => 'Upgrade Required',
'429' => 'Too Many Requests',
'500' => 'Internal Server Error',
'501' => 'Not Implemented',
'502' => 'Bad Gateway',
'503' => 'Service Unavailable',
'504' => 'Gateway Timeout',
'505' => 'HTTP Version Not Supported',
];
return isset($statusTexts[$code]) ? $statusTexts[$code] : null;
return ['internal/redirect', ['url' => $url]];
}
}

View file

@ -0,0 +1,59 @@
<?php
class Request
{
const GET = 'GET';
const POST = 'POST';
const HEAD = 'HEAD';
const OPTIONS = 'OPTIONS';
protected static $method;
public static function getMethod(): string
{
if (!static::$method)
{
$method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : null;
static::$method = in_array($method, [static::GET, static::POST, static::HEAD, static::OPTIONS]) ? $method : static::GET;
}
return static::$method;
}
public static function isGet(): bool
{
return static::getMethod() == static::GET;
}
public static function isPost(): bool
{
return static::getMethod() == static::POST;
}
public static function isCacheableMethod(): bool
{
return in_array(static::getMethod(), [static::GET, static::HEAD]);
}
public static function getOriginalIp(): string
{
return isset($_SERVER['HTTP_X_FORWARDED_FOR']) ?
$_SERVER['HTTP_X_FORWARDED_FOR'] :
(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '');
}
public static function getUserAgent(): string
{
return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
}
public static function getHost(): string
{
// apparently trailing period is legal: http://www.dns-sd.org/TrailingDotsInDomainNames.html
return isset($_SERVER['HTTP_HOST']) ? rtrim($_SERVER['HTTP_HOST'], '.') : '';
}
public static function getRelativeUri(): string
{
return isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
}
}

View file

@ -10,6 +10,7 @@ class ContentActions extends Actions
public static function executeHome(): array
{
Response::enableHttpCache(180);
return ['page/home', [
'totalUSD' => CreditApi::getTotalDollarSales(),
'totalPeople' => CreditApi::getTotalPeople()
@ -70,11 +71,10 @@ class ContentActions extends Actions
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
], [
'Content-Type' => 'text/xml; charset=utf-8'
]];
}
@ -161,14 +161,12 @@ class ContentActions extends Actions
$zip->close();
Response::enableHttpCache(180);
Response::setDownloadHttpHeaders($zipFileName, 'application/zip', filesize($zipPath));
return ['internal/zip', [
'_no_layout' => true,
'zipPath' => $zipPath
], [
'Content-Disposition' => 'attachment;filename=' . $zipFileName,
'X-Content-Type-Options' => 'nosniff',
'Content-Type' => 'application/zip',
'Content-Length' => filesize($zipPath),
]];
}

View file

@ -169,7 +169,7 @@ class DownloadActions extends Actions
protected static function guessOs()
{
//if exact OS is requested, use that
$uri = strtok($_SERVER['REQUEST_URI'], '?');
$uri = strtok(Request::getRelativeUri(), '?');
foreach (static::getOses() as $os => $osChoice)
{
if ($osChoice[0] == $uri)
@ -184,7 +184,7 @@ class DownloadActions extends Actions
}
//otherwise guess from UA
$ua = $_SERVER['HTTP_USER_AGENT'];
$ua = Request::getUserAgent();
if (stripos($ua, 'OS X') !== false)
{
return strpos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false ? static::OS_IOS : static::OS_OSX;

View file

@ -11,7 +11,7 @@ class MailActions extends Actions
{
$nextUrl = isset($_POST['returnUrl']) && $_POST['returnUrl'] ? $_POST['returnUrl'] : '/join-list';
if ($_SERVER['REQUEST_METHOD'] !== 'POST')
if (!Request::isPost())
{
return Controller::redirect($nextUrl);
}
@ -62,7 +62,7 @@ class MailActions extends Actions
public static function prepareJoinListPartial(array $vars)
{
$vars['listSig'] = md5(serialize($vars));
$vars += ['btnClass' => 'btn-primary', 'returnUrl' => $_SERVER['REQUEST_URI']];
$vars += ['btnClass' => 'btn-primary', 'returnUrl' => Request::getRelativeUri()];
if (Session::get(Session::KEY_LIST_SUB_SIGNATURE) == $vars['listSig'])
{

View file

@ -16,7 +16,7 @@ class NavActions extends Actions
public static function getNavUri()
{
return static::$navUri ?: $_SERVER['REQUEST_URI'];
return static::$navUri ?: Request::getRelativeUri();
}
public static function prepareFooterPartial(array $vars)

View file

@ -24,7 +24,7 @@ class i18n
{
if ($culture === null)
{
$urlTokens = $_SERVER['HTTP_HOST'] ? explode('.', $_SERVER['HTTP_HOST']) : [];
$urlTokens = Request::getHost() ? explode('.', Request::getHost()) : [];
$code = $urlTokens ? reset($urlTokens) : 'en';
switch($code)
{

View file

@ -61,8 +61,8 @@ class Prefinery
if (!$user)
{
// dont record ip for lbry.io addresses, for testing
$ip = isset($_SERVER['REMOTE_ADDR']) && !preg_match('/@lbry\.io$/', $email) ? $_SERVER['REMOTE_ADDR'] : null;
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null;
$ip = !preg_match('/@lbry\.io$/', $email) ? Request::getOriginalIp() : null;
$ua = Request::getUserAgent();
$user = Prefinery::createTester(array_filter([
'email' => $email,
'status' => $inviteCode ? static::STATE_ACTIVE: static::STATE_APPLIED, # yes, has to be ACTIVE to validate invite code

View file

@ -13,7 +13,7 @@ class Slack
$slackErrorNotificationUrl = Config::get('slack_error_notification_url');
if ($slackErrorNotificationUrl)
{
Curl::post($slackErrorNotificationUrl, ['text' => '<!everyone> ' . $_SERVER['REQUEST_URI'] . "\n" . $e], ['json_data' => true]);
Curl::post($slackErrorNotificationUrl, ['text' => '<!everyone> ' . Request::getRelativeUri() . "\n" . $e], ['json_data' => true]);
}
}
}

View file

@ -276,7 +276,7 @@ class Post
$cover = $this->getCover();
if ($cover)
{
$urls[] = 'https://' . $_SERVER['SERVER_NAME'] . '/img/blog-covers/' . $cover;
$urls[] = 'https://' . Request::getHost() . '/img/blog-covers/' . $cover;
}
$matches = [];

View file

@ -1,4 +1,5 @@
<?php
/**
* Description of Response
*
@ -6,7 +7,20 @@
*/
class Response
{
protected static $metaDescription = '',
const HEADER_STATUS = 'Status';
const HEADER_LOCATION = 'Location';
const HEADER_CACHE_CONTROL = 'Cache-Control';
const HEADER_LAST_MODIFIED = 'Last-Modified';
const HEADER_ETAG = 'Etag';
const HEADER_CONTENT_TYPE = 'Content-Type';
const HEADER_CONTENT_LENGTH = 'Content-Length';
const HEADER_CONTENT_DISPOSITION = 'Content-Disposition';
const HEADER_CONTENT_TYPE_OPTIONS = 'X-Content-Type-Options';
protected static
$metaDescription = '',
$metaTitle = '',
$jsCalls = [],
$assets = [
@ -16,6 +30,11 @@ class Response
],
'css' => []
],
$headers = [],
$headersSent = false,
$content = '',
$contentSent = false,
$isHeadersOnly = false,
// $bodyCssClasses = [],
$metaImages = [];
@ -31,7 +50,7 @@ class Response
public static function addMetaImages(array $urls)
{
foreach($urls as $url)
foreach ($urls as $url)
{
static::addMetaImage($url);
}
@ -61,7 +80,7 @@ class Response
{
$title = '';
preg_match_all('/<h(1|2)[^>]*>([^<]+)</', $content, $titleMatches);
foreach($titleMatches[1] as $matchIndex => $headerValue)
foreach ($titleMatches[1] as $matchIndex => $headerValue)
{
if ($headerValue == '1' || !$title)
{
@ -96,6 +115,225 @@ class Response
return static::$assets['js'];
}
public static function send()
{
static::sendHeaders();
static::sendContent();
}
public static function setContent(string $content)
{
static::$content = $content;
}
public static function getContent(): string
{
return static::$content;
}
public static function sendContent()
{
if (static::$contentSent)
{
throw new LogicException('Content has already been sent. It cannot be sent twice');
}
if (!static::$isHeadersOnly)
{
echo static::$content;
}
static::$contentSent = true;
}
public static function setIsHeadersOnly(bool $isHeadersOnly = true)
{
static::$isHeadersOnly = $isHeadersOnly;
}
public static function setDownloadHttpHeaders($name, $type = null, $size = null, $noSniff = true)
{
static::setHeaders(array_filter([
'Content-Disposition' => 'attachment;filename=' . $name,
'Content-Type' => $type ? 'application/zip' : null,
'Content-Length' => $size ?: null,
'X-Content-Type-Options' => $noSniff ? 'nosniff' : null,
]));
}
public static function setContentEtag()
{
static::setHeader(static::HEADER_ETAG, md5(static::getContent()));
}
public static function enableHttpCache(int $seconds = 300)
{
static::addCacheControlHeader('max-age', $seconds);
}
public static function addCacheControlHeader(string $name, $value = null)
{
$cacheControl = static::getHeader(static::HEADER_CACHE_CONTROL);
$currentHeaders = [];
if ($cacheControl)
{
foreach (preg_split('/\s*,\s*/', $cacheControl) as $tmp)
{
$tmp = explode('=', $tmp);
$currentHeaders[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : null;
}
}
$currentHeaders[strtr(strtolower($name), '_', '-')] = $value;
$headers = [];
foreach ($currentHeaders as $key => $currentVal)
{
$headers[] = $key . ($currentVal !== null ? '=' . $currentVal : '');
}
static::setHeader(static::HEADER_CACHE_CONTROL, implode(', ', $headers));
}
public static function setHeader($name, $value)
{
static::$headers[$name] = $value;
}
public static function setHeaders($headers, $overwrite = true)
{
foreach ($headers as $name => $value)
{
if ($overwrite || !static::getHeader($name))
{
static::setHeader($name, $value);
}
}
}
public static function getHeader($name, $default = null)
{
return isset(static::$headers[$name]) ? static::$headers[$name] : $default;
}
public static function getHeaders(): array
{
return static::$headers;
}
public static function setStatus($status)
{
static::setHeader(static::HEADER_STATUS, $status);
}
public static function setDefaultSecurityHeaders()
{
$defaultHeaders = [
'Content-Security-Policy' => "frame-ancestors 'none'",
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1',
];
if (IS_PRODUCTION)
{
$defaultHeaders['Strict-Transport-Security'] = 'max-age=31536000';
}
static::setHeaders($defaultHeaders, false);
}
public static function sendHeaders()
{
if (static::$headersSent)
{
throw new LogicException('Headers have already been sent. They cannot be sent twice');
}
$headers = static::getHeaders();
if (isset($headers[static::HEADER_STATUS]))
{
$status = 'HTTP/1.0 ' . $headers[static::HEADER_STATUS] . ' ' . static::getStatusTextForCode($headers[static::HEADER_STATUS]);
header($status);
if (substr(php_sapi_name(), 0, 3) == 'cgi')
{
// fastcgi servers cannot send this status information because it was sent by them already due to the HTT/1.0 line
// so we can safely unset them. see ticket #3191
unset($headers[static::HEADER_STATUS]);
}
}
foreach ($headers as $name => $value)
{
header($name . ': ' . $value);
}
static::$headersSent = true;
}
public static function getStatusTextForCode($code)
{
$statusTexts = [
'100' => 'Continue',
'101' => 'Switching Protocols',
'200' => 'OK',
'201' => 'Created',
'202' => 'Accepted',
'203' => 'Non-Authoritative Information',
'204' => 'No Content',
'205' => 'Reset Content',
'206' => 'Partial Content',
'300' => 'Multiple Choices',
'301' => 'Moved Permanently',
'302' => 'Found',
'303' => 'See Other',
'304' => 'Not Modified',
'305' => 'Use Proxy',
'306' => '(Unused)',
'307' => 'Temporary Redirect',
'400' => 'Bad Request',
'401' => 'Unauthorized',
'402' => 'Payment Required',
'403' => 'Forbidden',
'404' => 'Not Found',
'405' => 'Method Not Allowed',
'406' => 'Not Acceptable',
'407' => 'Proxy Authentication Required',
'408' => 'Request Timeout',
'409' => 'Conflict',
'410' => 'Gone',
'411' => 'Length Required',
'412' => 'Precondition Failed',
'413' => 'Request Entity Too Large',
'414' => 'Request-URI Too Long',
'415' => 'Unsupported Media Type',
'416' => 'Requested Range Not Satisfiable',
'417' => 'Expectation Failed',
'419' => 'Authentication Timeout',
'422' => 'Unprocessable Entity',
'426' => 'Upgrade Required',
'429' => 'Too Many Requests',
'500' => 'Internal Server Error',
'501' => 'Not Implemented',
'502' => 'Bad Gateway',
'503' => 'Service Unavailable',
'504' => 'Gateway Timeout',
'505' => 'HTTP Version Not Supported',
];
return isset($statusTexts[$code]) ? $statusTexts[$code] : null;
}
protected static function normalizeHeaderName($name)
{
return preg_replace_callback(
'/\-(.)/',
function ($matches) { return '-' . strtoupper($matches[1]); },
strtr(ucfirst(strtolower($name)), '_', '-')
);
}
// public static function addBodyCssClass($classOrClasses)
// {
// static::$bodyCssClasses = array_unique(array_merge(static::$bodyCssClasses, (array)$classOrClasses));

View file

@ -78,17 +78,17 @@ class View
return ParsedownExtra::instance()->text(trim(file_get_contents($path)));
}
public static function exists($template)
public static function exists($template): bool
{
return is_readable(static::getFullPath($template));
}
protected static function isMarkdown($nameOrPath)
protected static function isMarkdown($nameOrPath): bool
{
return strlen($nameOrPath) > 3 && substr($nameOrPath, -3) == '.md';
}
protected static function getFullPath($template)
protected static function getFullPath($template): string
{
if ($template && $template[0] == '/')
{
@ -103,12 +103,12 @@ class View
return ROOT_DIR . '/view/template/' . $template . '.php';
}
public static function imagePath($image)
public static function imagePath($image): string
{
return '/img/' . $image;
}
public static function parseMarkdown($template)
public static function parseMarkdown($template): array
{
$path = static::getFullPath($template);
list($ignored, $frontMatter, $markdown) = explode('---', file_get_contents($path), 3);

View file

@ -1,5 +1,6 @@
<?php $error = isset($error) ? $error : null ?>
<form action="/list-subscribe" method="post" novalidate>
<?php if ($error): ?>
<div class="notice notice-error spacer1"><?php echo $error ?></div>
<?php elseif ($success): ?>
@ -10,13 +11,14 @@
<?php js_end() ?>
<div class="notice notice-success spacer1"><?php echo $success ?></div>
<?php endif ?>
<?php if ($error || !$success): ?>
<div class="mail-submit">
<input type="hidden" name="returnUrl" value="<?php echo $returnUrl ?>"/>
<input type="hidden" name="listId" value="<?php echo $listId ?>"/>
<input type="hidden" name="listSig" value="<?php echo $listSig ?>"/>
<input type="email" value="" name="email" class="required email standard" placeholder= "<?php echo __('email.placeholder') ?>">
<input type="submit" value="<?php echo isset($submitLabel) ? $submitLabel : __('email.subs') ?>" name="subscribe" id="mc-embedded-subscribe" class="<?php echo $btnClass ?>">
<input type="submit" value="<?php echo isset($submitLabel) ? $submitLabel : __('email.subs') ?>" name="subscribe" class="<?php echo $btnClass ?>">
<?php if (isset($fbEvent)): ?>
<input type="hidden" name="fbEvent" value="<?php echo $fbEvent ?>" />
<?php endif ?>

View file

@ -23,13 +23,13 @@
<div class="span6">
<h3><?php echo __('publish.keepl') ?></h3>
<ul>
<?php if ($_SERVER['REQUEST_URI'] != '/what'): ?>
<?php if (Request::getRelativeUri() != '/what'): ?>
<li>Read "<a href="/what" class="link-primary">Art in the Internet Age</a>", an introductory essay.</li>
<?php endif ?>
<?php if ($_SERVER['REQUEST_URI'] != '/team'): ?>
<?php if (Request::getRelativeUri() != '/team'): ?>
<li>Find out about <a href="/team" class="link-primary">the team behind LBRY</a>.</li>
<?php endif ?>
<?php if (strpos($_SERVER['REQUEST_URI'], '/news') === false): ?>
<?php if (strpos(Request::getRelativeUri(), '/news') === false): ?>
<li>Check out the latest <a href="/news" class="link-primary">news</a>.</li>
<?php endif ?>
</ul>

View file

@ -21,7 +21,7 @@ try
{
View::compileCss();
}
Controller::dispatch(strtok($_SERVER['REQUEST_URI'], '?'));
Controller::dispatch(strtok(Request::getRelativeUri(), '?'));
}
catch(Throwable $e)
{