From 783a0bb7384805f1ba91a1271daea5f570eb7063 Mon Sep 17 00:00:00 2001 From: Simbiat Date: Tue, 14 Dec 2021 16:34:55 +0200 Subject: [PATCH] Added support for Permissions-Policy: as a separate signature and toggle for `features` `report-uri` is now sent only if flag is provided Fix for `Allow-Origin` headers when supplying an array Minor adjustments as per PHPStorm suggestions --- README.md | 4 +- doc/Headers.md | 10 ++-- src/HTTP20/Common.php | 42 ++++++++-------- src/HTTP20/HTML.php | 2 +- src/HTTP20/Headers.php | 103 +++++++++++++++++++++++++++++---------- src/HTTP20/Meta.php | 4 +- src/HTTP20/PrettyURL.php | 2 +- src/HTTP20/Sharing.php | 36 +++++--------- src/HTTP20/Sitemap.php | 4 +- 9 files changed, 126 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 41cbe98..cfb8972 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # HTTP20 Set of classes/functions that may be universally useful for websites (or some parts of them, at least). -They are provided in single library and not separately, not only because of some inter-dependencies, but also because if you are building a website from scratch, most likely, you will be interested in a bunch of them at the same time either way. +They are provided in single library and not separately, not only because of some interdependencies, but also because if you are building a website from scratch, most likely, you will be interested in a bunch of them at the same time either way. + +_Notice:_ While some functions (like `zEcho` or different `Headers`) can replace respective logic in server software (like Apache, nginx and such), it's not recommended using them for that because of performance downside. Only use them only if you do not have access to server settings, want to pre-generate the headers to then use in server settings, or you want to customize some of them for specific pages, that you can't target by the server software. - [HTTP20](#HTTP20) * [Atom](doc/Atom.md) diff --git a/doc/Headers.md b/doc/Headers.md index 98d8e1d..5f4d23e 100644 --- a/doc/Headers.md +++ b/doc/Headers.md @@ -73,7 +73,7 @@ performance(int $keepalive = 0, array $clientHints = []); ``` Sends some headers that may improve performance on client side. `$keepalive` is used for `Keep-Alive` header governing how long the connection should stay up. Header will be sent only if server is using HTTP version other than 2.0. -`$clientHints` instructs clients, that your server supports Client Hints (https://developer.mozilla.org/en-US/docs/Glossary/Client_hints) like DPR, Width, Viewport-Width, Downlink, .etc and client should cache the output accordingly, in order to increase allow cache hitting and thus improve performance. +`$clientHints` instructs clients, that your server supports Client Hints (https://developer.mozilla.org/en-US/docs/Glossary/Client_hints) like DPR, Width, Viewport-Width, Downlink, etc. and client should cache the output accordingly, in order to increase allow cache hitting and thus improve performance. ## security ```php @@ -113,19 +113,21 @@ default: ## contentPolicy ```php -contentPolicy(array $cspDirectives = [], bool $reportOnly = false); +contentPolicy(array $cspDirectives = [], bool $reportOnly = false, bool $reportUri = false); ``` Sends Content-Security-Policy header, that improves your page security. It's done separately from other security stuff, because unlike the rest of the headers this is usable only for HTML. `$cspDirectives` (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) allows you to provide a list of directives and their settings (with validation) to control CSP headers. By default, essentially everything is either disabled or allowed only from `self`, which give you a solid base in terms of restricting access. `$reportOnly` allows you to control, whether you only report (`Content-Security-Policy-Report-Only`) CPS violations or report **and** block them. Be default it's set as `false` for security enforcement. Note, that if it's set to `true`, but you do not provide `report-to` directive **no** CSP header will be sent, reducing your security. For that reason, if you do want to report, I can suggest using https://rapidsec.com/ which is free. Also note, that while `report-uri` is **temporary** added until `report-to` is supported by all browsers, `report-uri` **will be discarded** if it's provided without `report-to` to encourage the use of a modern directive. +`$reportUri` will add `report-uri` in the headers as well. It is deprecated, thus defaults to `false`, but if you want to support still - you can. ## features ```php -features(array $features = [], bool $forceCheck = true); +features(array $features = [], bool $forceCheck = true, bool $permissions = false); ``` -Allows controlling different features through Feature-Policy header. It should only be used, when sending HTML. +Allows controlling different features through `Feature-Policy` header. It should only be used, when sending HTML. `$features` expects associative array, where each key is name of the policy in lower case and value - expected `allow list`. If an empty array is sent, default values will be applied (most features are disabled). `$forceCheck` is added for futureproofing, but is enabled by default. If set to `true` will check if the feature is "supported" (present in default array) and value complies with the standard. Setting it to `false` will allow you to utilize a feature or value not yet supported by the library. +`$permissions` is a flag to toggle `Permissions-Policy`, which is replacement for `Feature-Policy` header. Alternatively you can use `features(array $features = [], bool $forceCheck = true)` signature, which will call `features` internally. ## secFetch ```php diff --git a/src/HTTP20/Common.php b/src/HTTP20/Common.php index 37ab477..d66f349 100644 --- a/src/HTTP20/Common.php +++ b/src/HTTP20/Common.php @@ -819,7 +819,7 @@ public function emailValidator(string $string): bool } } - #Function to check if string is an URI as per RFC 3986 + #Function to check if string is a URI as per RFC 3986 public function uriValidator(string $string): bool { if (preg_match('/^(?[a-z][a-z0-9+.-]+):(?\/\/(?[^@]+@)?(?[\p{L}0-9.\-_~]+)(?:\d+)?)?(?(?:[\p{L}0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=:@])+(?:\/(?:[\p{L}0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=:@])*)*|(?:\/(?:[\p{L}0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=:@])+)*)?(?\?(?:[\p{L}0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=:@]|[\/?])+)?(?#(?:[\p{L}0-9-._~]|%[a-f0-9]{2}|[!$&\'()*+,;=:@]|[\/?])+)?$/iu', $this->htmlToRFC3986($string)) === 1) { @@ -907,15 +907,15 @@ public function reductor(string|array $files, string $type, bool $minify = false $content = preg_replace( [ // Remove comment(s) - '#\s*("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')\s*|\s*\/\*(?!\!|@cc_on)(?>[\s\S]*?\*\/)\s*|\s*(?[\s\S]*?\*/)\s*|\s*(?.*?\*\/)|\/(?!\/)[^\n\r]*?\/(?=[\s.,;]|[gimuy]|$))|\s*([!%&*\(\)\-=+\[\]\{\}|;:,.<>?\/])\s*#s', + '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|/\*(?>.*?\*/)|/(?!/)[^\n\r]*?/(?=[\s.,;]|[gimuy]|$))|\s*([!%&*()\-=+\[\]{}|;:,.<>?/])\s*#s', // Remove the last semicolon - '#;+\}#', + '#;+}#', // Minify object attribute(s) except JSON attribute(s). From `{'foo':'bar'}` to `{foo:'bar'}` - '#([\{,])([\'])(\d+|[a-z_][a-z0-9_]*)\2(?=\:)#i', + '#([{,])([\'])(\d+|[a-z_][a-z0-9_]*)\2(?=:)#i', // --ibid. From `foo['bar']` to `foo.bar` - '#([a-z0-9_\)\]])\[([\'"])([a-z_][a-z0-9_]*)\2\]#i' + '#([a-z0-9_)\]])\[([\'"])([a-z_][a-z0-9_]*)\2]#i' ], [ '$1', @@ -930,26 +930,26 @@ public function reductor(string|array $files, string $type, bool $minify = false $content = preg_replace( [ // Remove comment(s) - '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s', + '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|/\*(?!!)(?>.*?\*/)|^\s*|\s*$#s', // Remove unused white-space(s) - '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|]?+=|[{};,>~]|\s(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|"(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si', + '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|/\*(?>.*?\*/))|\s*+;\s*+(})\s*+|\s*+([*$~^|]?+=|[{};,>~]|\s(?![0-9.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|"(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si', // Replace `0(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)` with `0` '#(?<=[\s:])(0)(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)#si', // Replace `:0 0 0 0` with `:0` - '#:(0\s+0|0\s+0\s+0\s+0)(?=[;\}]|\!important)#i', + '#:(0\s+0|0\s+0\s+0\s+0)(?=[;}]|!important)#i', // Replace `background-position:0` with `background-position:0 0` - '#(background-position):0(?=[;\}])#si', + '#(background-position):0(?=[;}])#si', // Replace `0.6` with `.6`, but only when preceded by `:`, `,`, `-` or a white-space '#(?<=[\s:,\-])0+\.(\d+)#s', // Minify string value - '#(\/\*(?>.*?\*\/))|(?.*?\*\/))|(\burl\()([\'"])([^\s]+?)\3(\))#si', + '#(/\*(?>.*?\*/))|(?.*?\*/))|(\burl\()([\'"])([^\s]+?)\3(\))#si', // Minify HEX color code '#(?<=[\s:,\-]\#)([a-f0-6]+)\1([a-f0-6]+)\2([a-f0-6]+)\3#i', // Replace `(border|outline):none` with `(border|outline):0` - '#(?<=[\{;])(border|outline):none(?=[;\}\!])#', + '#(?<=[{;])(border|outline):none(?=[;}!])#', // Remove empty selector(s) - '#(\/\*(?>.*?\*\/))|(^|[\{\}])(?:[^\s\{\}]+)\{\}#s' + '#(/\*(?>.*?\*/))|(^|[{}])(?:[^\s{}]+){}#s' ], [ '$1', @@ -980,15 +980,15 @@ function($matches) { '#<(img|input)(>| .*?>)#s', // Remove a line break and two or more white-space(s) between tag(s) '#()|(>)(?:\n*|\s{2,})(<)|^\s*|\s*$#s', - '#()|(?)\s+(<\/.*?>)|(<[^\/]*?>)\s+(?!\<)#s', // t+c || o+t - '#()|(<[^\/]*?>)\s+(<[^\/]*?>)|(<\/.*?>)\s+(<\/.*?>)#s', // o+o || c+c - '#()|(<\/.*?>)\s+(\s)(?!\<)|(?)\s+(\s)(<[^\/]*?\/?>)|(<[^\/]*?\/?>)\s+(\s)(?!\<)#s', // c+t || t+o || o+t -- separated by long white-space(s) - '#()|(<[^\/]*?>)\s+(<\/.*?>)#s', // empty tag - '#<(img|input)(>| .*?>)<\/\1>#s', // reset previous fix + '#()|(?)\s+()|(<[^/]*?>)\s+(?!<)#s', // t+c || o+t + '#()|(<[^/]*?>)\s+(<[^/]*?>)|()\s+()#s', // o+o || c+c + '#()|()\s+(\s)(?!<)|(?)\s+(\s)(<[^/]*?/?>)|(<[^/]*?/?>)\s+(\s)(?!<)#s', // c+t || t+o || o+t -- separated by long white-space(s) + '#()|(<[^/]*?>)\s+()#s', // empty tag + '#<(img|input)(>| .*?>)#s', // reset previous fix '#( ) (?![<\s])#', // clean up ... - '#(?<=\>)( )(?=\<)#', // --ibid + '#(?<=>)( )(?=<)#', // --ibid // Remove HTML comment(s) except IE comment(s) - '#\s*\s*|(?)\n+(?=\<[^!])#s' + '#\s*\s*|(?)\n+(?=<[^!])#s' ], [ '<$1$2', diff --git a/src/HTTP20/HTML.php b/src/HTTP20/HTML.php index b7c83f1..1611022 100644 --- a/src/HTTP20/HTML.php +++ b/src/HTTP20/HTML.php @@ -204,7 +204,7 @@ public function timeline(array $items, string $format = 'Y-m-d', bool $asc = fal #Process current events. Doing this here, because it's less important. if (!empty($current)) { #Check if there are finished events in timeline. If there are none - do not create links to "current" ones - if (array_search(0, array_column($toOrder, 'start')) !== false) { + if (in_array(0, array_column($toOrder, 'start'))) { $currentList = '
Ongoing: '; foreach ($current as $item) { #Generate id diff --git a/src/HTTP20/Headers.php b/src/HTTP20/Headers.php index e098085..dfa8777 100644 --- a/src/HTTP20/Headers.php +++ b/src/HTTP20/Headers.php @@ -10,7 +10,7 @@ class Headers public const safeMethods = ['GET', 'HEAD', 'POST']; #Full list of HTTP methods public const allMethods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']; - #List of headers we allow to expose by default + #List of headers we allow exposing by default public const exposedHeaders = [ #CORS allowed ones, except for Pragma and Expires, as those two are discouraged to be used (Cache-Control is far better) 'Cache-Control', 'Content-Language', 'Content-Type', 'Last-Modified', @@ -76,6 +76,37 @@ class Headers #Disabling APIs for modification of spatial navigation and scrolling, since you need them only for specific cases 'navigation-override' => '\'none\'', 'vertical-scroll' => '\'none\'', ]; + #Default values for Permissions-Policy, essentially disabling most of them. It is different from secureFeatures, because of slightly different values and different list of policies + public const permissionsDefault = [ + #Disable access to sensors + 'accelerometer' => '', 'ambient-light-sensor' => '', 'gyroscope' => '', 'magnetometer' => '', + #Disable access to devices + 'battery' => '', 'camera' => '', 'keyboard-map' => '', 'microphone' => '', 'midi' => '', 'usb' => '', 'gamepad' => '', 'speaker-selection' => '', 'hid' => '', 'serial' => '', 'window-placement' => '', + #Changing document.domain can allow some cross-origin access and is discouraged, due to existence of other (better) mechanisms + 'document-domain' => '', + #Allowing use of DRM and Web Authentication API, but only on our site and its own frames + 'encrypted-media' => 'self', 'publickey-credentials-get' => 'self', 'trust-token-redemption' => 'self', + #Disable geolocation, XR tracking, payment and screen capture APIs + 'geolocation' => '', 'xr-spatial-tracking' => '', 'payment' => '', 'display-capture' => '', + #Disable wake-locks + 'screen-wake-lock' => '', 'idle-detection' => '', + #Disable Web Share API. It's recommended to enable it explicitly for pages, where sharing will not expose potentially sensitive materials + 'web-share' => '', + #Disable synchronous XMLHttpRequests (that were technically deprecated) + 'sync-xhr' => '', + #Disable synchronous parsing blocking scripts (inline without defer/async attribute) + 'sync-script' => '', + #Disable autoplay, font swapping, fullscreen and picture-in-picture (if triggered in some automatic mode, can really annoy users) + 'autoplay' => '', 'fullscreen' => '', 'picture-in-picture' => '', + #Disable execution of scripts/task in elements, that are not rendered or visible + 'execution-while-not-rendered' => '', 'execution-while-out-of-viewport' => '', + #Disabling APIs for modification of spatial navigation and scrolling, since you need them only for specific cases + 'navigation-override' => '', 'vertical-scroll' => '', 'focus-without-user-activation' => '', + #Clipboard access. Enable only if you are going to manipulate clipboard on client side + 'clipboard-read' => '', 'clipboard-write' => '', + #User tracking stuff + 'cross-origin-isolated' => '', 'conversion-measurement' => '', 'interest-cohort' => '', + ]; #Values supported by Sandbox in CSP public const sandboxValues = ['allow-downloads-without-user-activation', 'allow-forms', 'allow-modals', 'allow-orientation-lock', 'allow-pointer-lock', 'allow-popups', 'allow-popups-to-escape-sandbox', 'allow-presentation', 'allow-same-origin', 'allow-scripts', 'allow-storage-access-by-user-activation', 'allow-top-navigation', 'allow-top-navigation-by-user-activation']; #Supported values for Sec-Fetch-* headers @@ -127,8 +158,8 @@ public function security(string $strat = 'strict', array $allowOrigins = [], arr #Vary is required by the standard. Using `false` to prevent overwriting of other Vary headers, if any were sent header('Vary: Origin', false); #Send actual headers - header('Access-Control-Allow-Origin: '.$allowOrigins); - header('Timing-Allow-Origin: '.$allowOrigins); + header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']); + header('Timing-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']); } else { #Send proper header denying access and stop processing $this->clientReturn('403'); @@ -181,7 +212,7 @@ public function security(string $strat = 'strict', array $allowOrigins = [], arr } #Function to process CSP header - public function contentPolicy(array $cspDirectives = [], bool $reportOnly = false): self + public function contentPolicy(array $cspDirectives = [], bool $reportOnly = false, bool $reportUri = false): self { #Set defaults directives for CSP $defaultDirectives = self::secureDirectives; @@ -221,7 +252,10 @@ public function contentPolicy(array $cspDirectives = [], bool $reportOnly = fals break; case 'report-to': $defaultDirectives['report-to'] = $value; - $defaultDirectives['report-uri'] = $value; + #This is only for legacy purposes, since report-uri is deprecated + if ($reportUri) { + $defaultDirectives['report-uri'] = $value; + } break; case 'report-uri': #Ensure that we do not use report-uri, unless there is a report-to, since report-uri is deprecated @@ -278,7 +312,7 @@ public function contentPolicy(array $cspDirectives = [], bool $reportOnly = fals } #Function to process Sec-Fetch headers. Arrays are set to empty ones by default for ease of use (sending empty array is a bit easier than copying values). - #$strict allows to enforce compliance with supported values only. Current W3C allows ignoring headers, if not sent or have unsupported values, but we may want to be stricter by setting this option to true + #$strict allows enforcing compliance with supported values only. Current W3C allows ignoring headers, if not sent or have unsupported values, but we may want to be stricter by setting this option to true #Below materials were used in preparation #https://www.w3.org/TR/fetch-metadata/ #https://fetch.spec.whatwg.org/ @@ -349,7 +383,7 @@ public function secFetch(array $site = [], array $mode = [], array $user = [], a $badRequest = true; } else { #There is also a recommendation to check whether a script-like is requesting certain MIME types - #Normally this should be done by browser, but we can do that as well and be independent from their logic + #Normally this should be done by browser, but we can do that as well and be independent of their logic if (!empty($_SERVER['HTTP_SEC_FETCH_DEST']) && in_array($_SERVER['HTTP_SEC_FETCH_DEST'], self::scriptLike)) { #Attempt to get content-type headers $contentType = ''; @@ -358,7 +392,7 @@ public function secFetch(array $site = [], array $mode = [], array $user = [], a $contentType = $_SERVER['HTTP_CONTENT_TYPE']; } else { #This is a standard header that should be present in PHP. Usually in case of POST method - if (isset($_SERVER['CONTENT_TYPE']) || isset($_SERVER['HTTP_CONTENT_TYPE'])) { + if (isset($_SERVER['CONTENT_TYPE'])) { $contentType = $_SERVER['CONTENT_TYPE']; } } @@ -366,13 +400,13 @@ public function secFetch(array $site = [], array $mode = [], array $user = [], a $mimeRegex = (new Common)::mimeRegex; #Check if we have already sent our own content-type header foreach (headers_list() as $header) { - if (preg_match('/^Content-type:/', $header) === 1) { + if (str_starts_with($header, 'Content-type:') === true) { #Get MIME $contentType = preg_replace('/^(Content-type:\s*)('.$mimeRegex.')$/', '$2', $header); break; } } - #If MIME is found and it matches CSV, audio, image or video - reject + #If MIME is found, and it matches CSV, audio, image or video - reject if (!empty($contentType) && preg_match('/(text\/csv)|((audio|image|video)\/[-+\w.]+)/', $contentType) === 1) { $badRequest = true; } @@ -404,7 +438,7 @@ public function performance(int $keepalive = 0, array $clientHints = []): self header('X-Content-Type-Options: nosniff'); #Allow DNS prefetch for some performance improvement on client side header('X-DNS-Prefetch-Control: on'); - #Keep-alive connection if not using HTTP2.0 (which prohibits it). Setting maximum number of connection as timeout power 1000. If a human is opening the pages, it's unlike he will be opening more than 1 page per second and it's unlikely that any page will have more than 1000 files linked to same server. If it does - some optimization may be required. + #Keep-alive connection if not using HTTP2.0 (which prohibits it). Setting maximum number of connection as timeout power 1000. If a human is opening the pages, it's unlike he will be opening more than 1 page per second, and it's unlikely that any page will have more than 1000 files linked to same server. If it does - some optimization may be required. if ($keepalive > 0 && $_SERVER['SERVER_PROTOCOL'] !== 'HTTP/2.0') { header('Connection: Keep-Alive'); header('Keep-Alive: timeout='.$keepalive.', max='.($keepalive*1000)); @@ -415,19 +449,30 @@ public function performance(int $keepalive = 0, array $clientHints = []): self #Notify, that we support Client Hints: https://developer.mozilla.org/en-US/docs/Glossary/Client_hints #Logic for processing them should be done outside this function, though header('Accept-CH: '.$clientHints); - #Instruct to vary cache depending on client hints + #Instruct cache to vary depending on client hints header('Vary: '.$clientHints, false); } return $this; } - #Function to manage Feature-Policy to control different features. By default most features are disabled for security and performance + #A wrapper for `features` with `permissions = true` just for convenience of access + #https://www.w3.org/TR/permissions-policy-1/ is replacement for Feature-Policy + public function permissions(array $features = [], bool $forceCheck = true): self + { + return $this->features($features, $forceCheck, true); + } + + #Function to manage Feature-Policy to control different features. By default, most features are disabled for security and performance #https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy #https://feature-policy-demos.appspot.com/ #https://featurepolicy.info/ - public function features(array $features = [], bool $forceCheck = true):self + public function features(array $features = [], bool $forceCheck = true, bool $permissions = false):self { - $defaults = self::secureFeatures; + if ($permissions) { + $defaults = self::permissionsDefault; + } else { + $defaults = self::secureFeatures; + } foreach ($features as $feature=>$allowList) { #Sanitize $feature = strtolower(trim($feature)); @@ -441,9 +486,17 @@ public function features(array $features = [], bool $forceCheck = true):self #Generate line for header $headerLine = ''; foreach ($defaults as $feature=>$allowList) { - $headerLine .= $feature.' '.$allowList.'; '; + if ($permissions) { + $headerLine .= $feature.'=('.$allowList.'), '; + } else { + $headerLine .= $feature.' '.$allowList.'; '; + } + } + if ($permissions) { + header('Permissions-Policy: ' . rtrim(trim($headerLine), ',')); + } else { + header('Feature-Policy: ' . trim($headerLine)); } - header('Feature-Policy: '.trim($headerLine)); return $this; } @@ -457,8 +510,8 @@ public function lastModified(int|string $modTime = 0, bool $exit = false): self $modTime = intval($modTime); } if ($modTime <= 0) { - #Get freshest modification time of all PHP files used ot PHP's getlastmod time - $modTime = max(max(array_map('filemtime', array_filter(get_included_files(), 'is_file'))), getlastmod()); + #Get the freshest modification time of all PHP files used ot PHP's getlastmod time + $modTime = max(array_map('filemtime', array_filter(get_included_files(), 'is_file')), getlastmod()); } #Send header header('Last-Modified: '.gmdate(\DATE_RFC7231, $modTime)); @@ -477,7 +530,7 @@ public function lastModified(int|string $modTime = 0, bool $exit = false): self public function cacheControl(string $string = '', string $cacheStrat = '', bool $exit = false): self { #Send headers related to cache based on strategy selected - #Some of the strategies are derived from https://csswizardry.com/2019/03/cache-control-for-civilians/ + #Some strategies are derived from https://csswizardry.com/2019/03/cache-control-for-civilians/ switch (strtolower($cacheStrat)) { case 'aggressive': header('Cache-Control: max-age=31536000, immutable, no-transform'); @@ -525,7 +578,7 @@ public function eTag(string $etag, bool $exit = false): self $this->clientReturn('304', $exit); } } - #Return error if If-Match was sent and it's different from our etag + #Return error if If-Match was sent, and it's different from our etag if (isset($_SERVER['HTTP_IF_MATCH'])) { if (trim($_SERVER['HTTP_IF_MATCH']) !== $etag) { $this->clientReturn('412', $exit); @@ -542,13 +595,13 @@ public function clientReturn(string $code = '500 Internal Server Error', bool $e if (isset(self::HTTPCodes[$code])) { $response = $code.' '.self::HTTPCodes[$code]; } else { - #Non standard code without text, not compliant with the standard + #Non-standard code without text, not compliant with the standard $response = '500 Internal Server Error'; } } else { $response = $code; } - #If response doe snot comply with HTTP standard - replace it with 500 + #If response does not comply with HTTP standard - replace it with 500 if (preg_match('/^([12345]\d{2})( .+)$/', $response) !== 1) { $response = '500 Internal Server Error'; } @@ -848,7 +901,7 @@ public function links(array $links = [], string $type = 'header', bool $strictRe #Function to handle Accept request header public function notAccept(array $supported = ['text/html'], bool $exit = true): bool|string { - #Check if header is set and we do have a limit on supported MIME types + #Check if header is set, and we do have a limit on supported MIME types if (isset($_SERVER['HTTP_ACCEPT']) && !empty($supported)) { #text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 #Generate list of acceptable values @@ -877,7 +930,7 @@ public function notAccept(array $supported = ['text/html'], bool $exit = true): return $this->clientReturn('406', $exit); } } else { - #Get the one with highest priority and return its value + #Get the one with the highest priority and return its value return array_keys($acceptable, max($acceptable))[0]; } } else { diff --git a/src/HTTP20/Meta.php b/src/HTTP20/Meta.php index 17112f6..129b058 100644 --- a/src/HTTP20/Meta.php +++ b/src/HTTP20/Meta.php @@ -28,7 +28,7 @@ public function twitter(array $general, array $playerApp = [], bool $pretty = fa $output .= ''; #Add site if not empty and valid if (!empty($general['site']) && preg_match('/^@?(\w){4,15}$/', $general['site']) === 1 && preg_match('/^.*(twitter|admin).*$/i', $general['site']) === 0) { - $output .= ''; + $output .= ''; } #Add site:id if not empty and valid if (!empty($general['site:id']) && preg_match('/^\d+$/', $general['site:id']) === 1) { @@ -36,7 +36,7 @@ public function twitter(array $general, array $playerApp = [], bool $pretty = fa } #Add creator if not empty and valid if (!empty($general['creator']) && preg_match('/^@?(\w){4,15}$/', $general['creator']) === 1 && preg_match('/^.*(twitter|admin).*$/i', $general['creator']) === 0) { - $output .= ''; + $output .= ''; } #Add creator:id if not empty and valid if (!empty($general['creator:id']) && preg_match('/^\d+$/', $general['creator:id']) === 1) { diff --git a/src/HTTP20/PrettyURL.php b/src/HTTP20/PrettyURL.php index a7f5e9b..d203774 100644 --- a/src/HTTP20/PrettyURL.php +++ b/src/HTTP20/PrettyURL.php @@ -4,7 +4,7 @@ class PrettyURL { - private string $urlUnsafe = '\+\*\'\(\);\/\?:@=&"<>#%{}\|\\\\\^~\[\]`'; + private string $urlUnsafe = '\+\*\'\(\);/\?:@=&"<>#%{}\|\\\\\^~\[]`'; private array $needles; private array $replaces = [ 'ꭕ'=>'x', diff --git a/src/HTTP20/Sharing.php b/src/HTTP20/Sharing.php index 851d73e..aff5914 100644 --- a/src/HTTP20/Sharing.php +++ b/src/HTTP20/Sharing.php @@ -74,12 +74,7 @@ public function download(string $file, string $filename = '', string $mime = '', #Check if it's empty again (or was from the start) if (empty($mime)) { #If not, attempt to check if in the constant list based on extension - if (isset($this->extToMime[$fileinfo['extension']])) { - $mime = $this->extToMime[$fileinfo['extension']]; - } else { - #Replace MIME - $mime = 'application/octet-stream'; - } + $mime = $this->extToMime[$fileinfo['extension']] ?? 'application/octet-stream'; } #Get file name if (empty($filename)) { @@ -112,7 +107,7 @@ public function download(string $file, string $filename = '', string $mime = '', if ($output === false) { return (new Headers)->clientReturn('500', $exit); } - #Disable buffering. This should help limiting the memory usage. At least, in some cases. + #Disable buffering. This should help limit the memory usage. At least, in some cases. stream_set_read_buffer($stream, 0); stream_set_write_buffer($output, 0); if (!empty($ranges)) { @@ -416,10 +411,9 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool } else { #Remove the file from list unset($_FILES[$field][$key]); - continue; } } else { - #Set new name for the file. By default we will be using hash of the file. Using sha3-256 since it has lower probability of collisions than md5, although we do lose some speed + #Set new name for the file. By default, we will be using hash of the file. Using sha3-256 since it has lower probability of collisions than md5, although we do lose some speed #Hash is saved regardless, though, since it may be very useful $_FILES[$field][$key]['hash'] = hash_file('sha3-256', $file['tmp_name']); if ($preserveNames) { @@ -450,7 +444,6 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool } else { #Remove the file from list unset($_FILES[$field][$key]); - continue; } } } else { @@ -460,7 +453,6 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool } #Remove the file from global list unset($_FILES[$field][$key]); - continue; } } } @@ -499,7 +491,7 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool } } #Process PUT requests - } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') { + } else { if (!isset($_SERVER['CONTENT_LENGTH']) || intval($_SERVER['CONTENT_LENGTH']) === 0) { return (new Headers)->clientReturn('411', $exit); } @@ -589,10 +581,10 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool fclose($stream); return (new Headers)->clientReturn('500', $exit); } - #Disable buffering. This should help limiting the memory usage. At least, in some cases. + #Disable buffering. This should help limit the memory usage. At least, in some cases. stream_set_read_buffer($stream, 0); stream_set_write_buffer($output, 0); - #Ignore user abort to attempt identify when client has aborted + #Ignore user abort to attempt to identify when client has aborted #ignore_user_abort(true); #Save file $result = stream_copy_to_stream($stream, $output, $client_size - $offset); @@ -621,11 +613,7 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool return (new Headers)->clientReturn('500', $exit); } else { #Get file MIME type - if (isset($_SERVER['CONTENT_TYPE'])) { - $filetype = $_SERVER['CONTENT_TYPE']; - } else { - $filetype = 'application/octet-stream'; - } + $filetype = $_SERVER['CONTENT_TYPE'] ?? 'application/octet-stream'; if (extension_loaded('fileinfo')) { $filetype = mime_content_type($this->uploadDir.'/'.$name); } @@ -669,7 +657,7 @@ public function upload(string|array $destPath, bool $preserveNames = false, bool #Function to copy data in small chunks (not HTTP1.1 chunks) based on speed limitation public function streamCopy($input, $output, int $totalSize = 0, int $offset = 0, int $speed = 10485760): bool|int { - #Ignore user abort to attempt identify when client has aborted + #Ignore user abort to attempt to identify when client has aborted ignore_user_abort(true); #Check that we have resources, since PHP does not have type hinting for resources if (!is_resource($input) || !is_resource($output)) { @@ -804,7 +792,7 @@ public function rangesValidate(int $size): array } } } - #If something went wrong and we got an empty range here - return as false + #If something went wrong, and we got an empty range here - return as false if (empty($ranges)) { return [0 => false]; } else { @@ -839,7 +827,7 @@ public function fileEcho(string $filepath, array $allowedMime = [], string $cach } } } - #While above checks actual MIME type it may be different from the one client may be expecting based on extension. For example RSS file will be recognized as application/xml (or text/xml), instead of application/rss+xml. This may be minor, but depending on client can cause unexpected behaviour. Thus we rely on extension here, since it can provide a more appropriate MIME type + #While above checks actual MIME type it may be different from the one client may be expecting based on extension. For example RSS file will be recognized as application/xml (or text/xml), instead of application/rss+xml. This may be minor, but depending on client can cause unexpected behaviour. Thus, we rely on extension here, since it can provide a more appropriate MIME type $extension = pathinfo($filepath)['extension']; #Set MIME from extension, of available if (!empty($extension) && !empty($this->extToMime[$extension])) { @@ -893,7 +881,7 @@ public function fileEcho(string $filepath, array $allowedMime = [], string $cach exit; } #Send data - if (fpassthru($stream) === false) { + if (fpassthru($stream) === 0) { (new Headers)->clientReturn('500', $exit); return 500; } @@ -954,7 +942,7 @@ public function proxyFile(string $url, string $cacheStrat = ''): void } } #Open streams - #Supress warning for $url, since connection can be refused for some reason and it still may be "normal" + #Supress warning for $url, since connection can be refused for some reason, and it still may be "normal" $url = @fopen($url, 'rb', context: stream_context_create(['http' => [ 'method' => 'GET', 'follow_location' => 1, diff --git a/src/HTTP20/Sitemap.php b/src/HTTP20/Sitemap.php index 6c4cd46..7f19dd1 100644 --- a/src/HTTP20/Sitemap.php +++ b/src/HTTP20/Sitemap.php @@ -11,7 +11,7 @@ public function sitemap(array $links, string $format = 'xml', bool $directOutput if (!in_array($format, ['xml', 'index', 'html', 'text', 'txt'])) { $format = 'xml'; } - #Validate links, if list is not empty. I did not find any recommendations for empty sitemaps and I do not see a technical reason to break here, because if sitemaps are generated using some kind of pagination logic and a "bad" page is server to it, that results in empty array + #Validate links, if list is not empty. I did not find any recommendations for empty sitemaps, and I do not see a technical reason to break here, because if sitemaps are generated using some kind of pagination logic and a "bad" page is server to it, that results in empty array $this->linksValidator($links); #Allow only 50000 links $links = array_slice($links, 0, 50000, true); @@ -42,7 +42,7 @@ public function sitemap(array $links, string $format = 'xml', bool $directOutput }; #Get its length $lenToAdd = strlen($toAdd); - #Check, that we are not exceeding the limit of 50MB. Using limit from Google (https://developers.google.com/search/docs/advanced/sitemaps/build-sitemap) rather then from original spec (https://www.sitemaps.org/protocol.html), since we should care more about search engines' limitations + #Check, that we are not exceeding the limit of 50 MB. Using limit from Google (https://developers.google.com/search/docs/advanced/sitemaps/build-sitemap) rather than from original spec (https://www.sitemaps.org/protocol.html), since we should care more about search engines' limitations if (($strLen + $lenToAdd) < 52428800) { $output .= $toAdd; $strLen += $lenToAdd;