v1/web/modules/contrib/fast_404/src/Fast404.php

295 lines
9.2 KiB
PHP

<?php
namespace Drupal\fast404;
use Drupal\Core\Site\Settings;
use Drupal\Core\Database\Database;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Component\Render\FormattableMarkup;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* Fast404: A value object for manager Fast 404 logic.
*
* @package Drupal\fast404
*/
class Fast404 {
use StringTranslationTrait;
/**
* Whether Fast 404 logic should be used.
*
* @var bool
*/
public $respond404 = FALSE;
/**
* Whether to load html or respond otherwise.
*
* @var bool
*/
public $loadHtml = TRUE;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* Language Negotiation Url Info.
*
* @var array
*/
protected static $languageNegotiationUrlInfo;
/**
* Fast404 constructor.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*/
public function __construct(Request $request) {
$this->request = $request;
}
/**
* Extension check.
*
* A strategy for handling Fast 404 settings.
*/
public function extensionCheck() {
// Get the path from the request.
$path = $this->request->getPathInfo();
// Ignore calls to the homepage, to avoid unnecessary processing.
if (!isset($path) || $path == '/') {
return;
}
// Check to see if the URL is that of an image derivative.
// If this file does not already exist, it will be handled via Drupal.
if (strpos($path, 'styles/')) {
// Check to see if we will allow anon users to access this page.
if (!Settings::get('fast404_allow_anon_imagecache', TRUE)) {
$cookies = $this->request->cookies->all();
// At this stage of the game we don't know if the user is logged in via
// regular function calls. Simply look for a session cookie. If we find
// one we'll assume they're logged in.
if (isset($cookies) && is_array($cookies)) {
foreach ($cookies as $cookie) {
if (stristr($cookie, 'SESS')) {
return;
}
}
}
}
// We're allowing anyone to hit non-existing image derivative URLs
// (default behavior).
else {
return;
}
}
// If we are using URL whitelisting then determine if the current URL is
// whitelisted before running the extension check.
// Check for exact URL matches and assume it's fine if we get one.
if (Settings::get('fast404_url_whitelisting', FALSE)) {
$trimmed_path = ltrim($path, '/');
$allowed = Settings::get('fast404_whitelist', []);
if (in_array($trimmed_path, $allowed)) {
// URL is whitelisted. Assumed good.
return TRUE;
}
}
// Check for whitelisted strings in the URL.
$string_whitelist = Settings::get('fast404_string_whitelisting', FALSE);
if (is_array($string_whitelist)) {
foreach ($string_whitelist as $str) {
if (strstr($path, $str) !== FALSE) {
return;
}
}
}
$extensions = Settings::get('fast404_exts', '/^(?!\/robots)^(?!\/system\/files).*\.(txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i');
// Determine if URL contains a blacklisted extension.
if (isset($extensions) && preg_match($extensions, $path, $m)) {
$this->loadHtml = FALSE;
$this->blockPath();
return;
}
}
/**
* Path check.
*
* Since the path check is a lot more aggressive in its blocking we should
* actually check that the user wants it to be done.
*/
public function pathCheck() {
if (!Settings::get('fast404_path_check', FALSE)) {
return;
}
// Get the path from the request.
$path = $this->request->getPathInfo();
// Ignore calls to the homepage, to avoid unnecessary processing.
if (!isset($path) || $path == '/') {
return;
}
// Translation url code pages.
if (!empty(self::$languageNegotiationUrlInfo)) {
// Get the path from the request.
$path = $this->request->getPathInfo();
// Separate the language path prefix if it exists.
$pos = strpos($path, '/', 1);
if ($pos !== FALSE) {
$prefix = substr($path, 1, $pos - 1);
}
else {
$prefix = substr($path, 1);
}
// If this string is one of the configured language prefixes, ignore it.
if (in_array($prefix, self::$languageNegotiationUrlInfo['prefixes'])) {
if ($pos !== FALSE) {
$path = substr($path, $pos);
if (empty($path) || $path == '/') {
// This path is the front page for a language prefix.
return;
}
}
else {
// This path is the front page for a language prefix.
return;
}
}
}
// If we have a database connection we can use it, otherwise we might be
// initialising it. We remove '/' from the list of possible patterns as it
// exists in the router by default. This means that the query would match
// any path (/%) which is undesirable.
$sql = "SELECT pattern_outline FROM {router} WHERE :path LIKE CONCAT(pattern_outline, '%') AND pattern_outline != '/'";
$result = Database::getConnection()->query($sql, [':path' => $path])->fetchField();
if ($result) {
return;
}
// Check the URL alias table for anything that's not a standard Drupal path.
// Remove any trailing slash found in the request path.
$path_noslash = rtrim($path, '/');
$sql = "SELECT id FROM {path_alias} WHERE alias = :alias";
$result = Database::getConnection()->query($sql, [':alias' => $path_noslash])->fetchField();
if ($result) {
return;
}
// Check for redirects if set to respect them.
if (Settings::get('fast404_respect_redirect', FALSE)) {
$path_noslash = trim(urldecode($path), '/');
// If the path equals the prefix, we are probaby on a language without a
// prefix.
$prefix = (isset($prefix) && $prefix !== $path_noslash) ? $prefix : '';
$sql = "SELECT rid FROM {redirect} WHERE redirect_source__path = :path";
$args = [
':path' => $path_noslash,
];
$language = array_search($prefix, self::$languageNegotiationUrlInfo['prefixes'] ?? [], TRUE);
if ($language) {
$sql .= " AND language = :language";
$args[':language'] = $language;
}
$result = Database::getConnection()->query($sql, $args)->fetchField();
if ($result) {
return;
}
}
// If we get to here it means nothing has matched the request so we assume
// it's a bad path and block it.
$this->blockPath();
}
/**
* Block the delivery of this 404 response.
*/
public function blockPath() {
$this->respond404 = TRUE;
}
/**
* Make sure cli calls are not blocked.
*
* @return bool
* Whether the path is blocked or not.
*/
public function isPathBlocked() {
if ($this->isCli()) {
return FALSE;
}
return $this->respond404;
}
/**
* Prepare a 404 response.
*
* @param bool $return
* Decide whether to return the response object or simply send it.
*
* @return \Symfony\Component\HttpFoundation\Response
* If this returns anything, it will be a response object.
*/
public function response($return = FALSE) {
$message = Settings::get('fast404_html', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL "@path" was not found on this server.</p></body></html>');
$return_gone = Settings::get('fast404_return_gone', FALSE);
$custom_404_path = Settings::get('fast404_HTML_error_page', FALSE);
// If a file is set to provide us with Fast 404 joy, load it.
if (($this->loadHtml || Settings::get('fast404_HTML_error_all_paths', FALSE) === TRUE) && file_exists($custom_404_path)) {
$message = @file_get_contents($custom_404_path, FALSE);
}
$headers = [
Settings::get('fast404_HTTP_status_method', 'mod_php') === 'FastCGI' ? 'Status:' : 'HTTP/1.0' => $return_gone ? '410 Gone' : '404 Not Found',
];
$response = new Response(new FormattableMarkup($message, ['@path' => $this->request->getPathInfo()]), $return_gone ? 410 : 404, $headers);
if ($return) {
return $response;
}
$response->send();
throw new ServiceUnavailableHttpException(3, 'The requested URL "@path" was not found on this server. Try again shortly.', ['@path' => $this->request->getPathInfo()]);
}
/**
* Check the type of interface between web server and PHP is CLI.
*
* @return bool
* Whether or not the Server API for this build of PHP is CLI.
*/
protected function isCli() {
return PHP_SAPI === 'cli';
}
/**
* Set $languageNegotiationUrlInfo property.
*
* @param array $lang_negotiation_url_info
* The lang url config.
*/
public function setLanguageNegotiationUrlInfo(array $lang_negotiation_url_info) {
if (!isset(self::$languageNegotiationUrlInfo)) {
self::$languageNegotiationUrlInfo = $lang_negotiation_url_info;
}
}
}