updated core to 7.73

This commit is contained in:
2020-09-24 17:04:08 +02:00
parent 084d28842a
commit 7d87153100
524 changed files with 25194 additions and 7745 deletions
+83 -8
View File
@@ -14,6 +14,8 @@
Drupal.ajax = Drupal.ajax || {};
Drupal.settings.urlIsAjaxTrusted = Drupal.settings.urlIsAjaxTrusted || {};
/**
* Attaches the Ajax behavior to each Ajax form element.
*/
@@ -130,6 +132,11 @@ Drupal.ajax = function (base, element, element_settings) {
// 5. /nojs# - Followed by a fragment.
// E.g.: path/nojs#myfragment
this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
// If the 'nojs' version of the URL is trusted, also trust the 'ajax' version.
if (Drupal.settings.urlIsAjaxTrusted[element_settings.url]) {
Drupal.settings.urlIsAjaxTrusted[this.url] = true;
}
this.wrapper = '#' + element_settings.wrapper;
// If there isn't a form, jQuery.ajax() will be used instead, allowing us to
@@ -142,7 +149,7 @@ Drupal.ajax = function (base, element, element_settings) {
// The 'this' variable will not persist inside of the options object.
var ajax = this;
ajax.options = {
url: ajax.url,
url: Drupal.sanitizeAjaxUrl(ajax.url),
data: ajax.submit,
beforeSerialize: function (element_settings, options) {
return ajax.beforeSerialize(element_settings, options);
@@ -155,26 +162,67 @@ Drupal.ajax = function (base, element, element_settings) {
ajax.ajaxing = true;
return ajax.beforeSend(xmlhttprequest, options);
},
success: function (response, status) {
success: function (response, status, xmlhttprequest) {
// Sanity check for browser support (object expected).
// When using iFrame uploads, responses must be returned as a string.
if (typeof response == 'string') {
response = $.parseJSON(response);
}
// Prior to invoking the response's commands, verify that they can be
// trusted by checking for a response header. See
// ajax_set_verification_header() for details.
// - Empty responses are harmless so can bypass verification. This avoids
// an alert message for server-generated no-op responses that skip Ajax
// rendering.
// - Ajax objects with trusted URLs (e.g., ones defined server-side via
// #ajax) can bypass header verification. This is especially useful for
// Ajax with multipart forms. Because IFRAME transport is used, the
// response headers cannot be accessed for verification.
if (response !== null && !Drupal.settings.urlIsAjaxTrusted[ajax.url]) {
if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
var customMessage = Drupal.t("The response failed verification so will not be processed.");
return ajax.error(xmlhttprequest, ajax.url, customMessage);
}
}
return ajax.success(response, status);
},
complete: function (response, status) {
complete: function (xmlhttprequest, status) {
ajax.ajaxing = false;
if (status == 'error' || status == 'parsererror') {
return ajax.error(response, ajax.url);
return ajax.error(xmlhttprequest, ajax.url);
}
},
dataType: 'json',
jsonp: false,
type: 'POST'
};
// For multipart forms (e.g., file uploads), jQuery Form targets the form
// submission to an iframe instead of using an XHR object. The initial "src"
// of the iframe, prior to the form submission, is set to options.iframeSrc.
// "about:blank" is the semantically correct, standards-compliant, way to
// initialize a blank iframe; however, some old IE versions (possibly only 6)
// incorrectly report a mixed content warning when iframes with an
// "about:blank" src are added to a parent document with an https:// origin.
// jQuery Form works around this by defaulting to "javascript:false" instead,
// but that breaks on Chrome 83, so here we force the semantically correct
// behavior for all browsers except old IE.
// @see https://www.drupal.org/project/drupal/issues/3143016
// @see https://github.com/jquery-form/form/blob/df9cb101b9c9c085c8d75ad980c7ff1cf62063a1/jquery.form.js#L68
// @see https://bugs.chromium.org/p/chromium/issues/detail?id=1084874
// @see https://html.spec.whatwg.org/multipage/browsers.html#creating-browsing-contexts
// @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
if (navigator.userAgent.indexOf("MSIE") === -1) {
ajax.options.iframeSrc = 'about:blank';
}
// Bind the ajaxSubmit function to the element event.
$(ajax.element).bind(element_settings.event, function (event) {
if (!Drupal.settings.urlIsAjaxTrusted[ajax.url] && !Drupal.urlIsLocal(ajax.url)) {
throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {'!url': ajax.url}));
}
return ajax.eventResponse(this, event);
});
@@ -348,7 +396,7 @@ Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
// this is only needed for IFRAME submissions.
var v = $.fieldValue(this.element);
if (v !== null) {
options.extraData[this.element.name] = v;
options.extraData[this.element.name] = Drupal.checkPlain(v);
}
}
@@ -447,8 +495,8 @@ Drupal.ajax.prototype.getEffect = function (response) {
/**
* Handler for the form redirection error.
*/
Drupal.ajax.prototype.error = function (response, uri) {
alert(Drupal.ajaxError(response, uri));
Drupal.ajax.prototype.error = function (xmlhttprequest, uri, customMessage) {
Drupal.displayAjaxError(Drupal.ajaxError(xmlhttprequest, uri, customMessage));
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
@@ -462,7 +510,7 @@ Drupal.ajax.prototype.error = function (response, uri) {
$(this.element).removeClass('progress-disabled').removeAttr('disabled');
// Reattach behaviors, if they were detached in beforeSerialize().
if (this.form) {
var settings = response.settings || this.settings || Drupal.settings;
var settings = this.settings || Drupal.settings;
Drupal.attachBehaviors(this.form, settings);
}
};
@@ -616,6 +664,33 @@ Drupal.ajax.prototype.commands = {
.removeClass('odd even')
.filter(':even').addClass('odd').end()
.filter(':odd').addClass('even');
},
/**
* Command to add css.
*
* Uses the proprietary addImport method if available as browsers which
* support that method ignore @import statements in dynamically added
* stylesheets.
*/
add_css: function (ajax, response, status) {
// Add the styles in the normal way.
$('head').prepend(response.data);
// Add imports in the styles using the addImport method if available.
var match, importMatch = /^@import url\("(.*)"\);$/igm;
if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
importMatch.lastIndex = 0;
while (match = importMatch.exec(response.data)) {
document.styleSheets[0].addImport(match[1]);
}
}
},
/**
* Command to update a form's build ID.
*/
updateBuildId: function(ajax, response, status) {
$('input[name="form_build_id"][value="' + response['old'] + '"]').val(response['new']);
}
};
+11 -6
View File
@@ -114,6 +114,7 @@ Drupal.jsAC.prototype.onkeyup = function (input, e) {
*/
Drupal.jsAC.prototype.select = function (node) {
this.input.value = $(node).data('autocompleteValue');
$(this.input).trigger('autocompleteSelect', [node]);
};
/**
@@ -167,7 +168,7 @@ Drupal.jsAC.prototype.unhighlight = function (node) {
Drupal.jsAC.prototype.hidePopup = function (keycode) {
// Select item if the right key or mousebutton was pressed.
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
this.input.value = $(this.selected).data('autocompleteValue');
this.select(this.selected);
}
// Hide popup.
var popup = this.popup;
@@ -220,7 +221,7 @@ Drupal.jsAC.prototype.found = function (matches) {
for (key in matches) {
$('<li></li>')
.html($('<div></div>').html(matches[key]))
.mousedown(function () { ac.select(this); })
.mousedown(function () { ac.hidePopup(this); })
.mouseover(function () { ac.highlight(this); })
.mouseout(function () { ac.unhighlight(this); })
.data('autocompleteValue', key)
@@ -270,8 +271,11 @@ Drupal.ACDB.prototype.search = function (searchString) {
var db = this;
this.searchString = searchString;
// See if this string needs to be searched for anyway.
searchString = searchString.replace(/^\s+|\s+$/, '');
// See if this string needs to be searched for anyway. The pattern ../ is
// stripped since it may be misinterpreted by the browser.
searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
// Skip empty search strings, or search strings ending with a comma, since
// that is the separator between search terms.
if (searchString.length <= 0 ||
searchString.charAt(searchString.length - 1) == ',') {
return;
@@ -293,8 +297,9 @@ Drupal.ACDB.prototype.search = function (searchString) {
// encodeURIComponent to allow autocomplete search terms to contain slashes.
$.ajax({
type: 'GET',
url: db.uri + '/' + Drupal.encodePath(searchString),
url: Drupal.sanitizeAjaxUrl(db.uri + '/' + Drupal.encodePath(searchString)),
dataType: 'json',
jsonp: false,
success: function (matches) {
if (typeof matches.status == 'undefined' || matches.status != 0) {
db.cache[searchString] = matches;
@@ -306,7 +311,7 @@ Drupal.ACDB.prototype.search = function (searchString) {
}
},
error: function (xmlhttp) {
alert(Drupal.ajaxError(xmlhttp, db.uri));
Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri));
}
});
}, this.delay);
@@ -0,0 +1,4 @@
/vendor/
/phpunit.xml
/.composer.lock
@@ -0,0 +1,20 @@
language: php
sudo: false
php:
- '5.3'
- '5.4'
- '5.5'
- '5.6'
- '7.0'
- '7.1'
before_install:
- phpenv config-rm xdebug.ini
- composer self-update
install:
- composer install
script: phpunit
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Denis Brumann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,61 @@
Polyfill unserialize [![Build Status](https://travis-ci.org/dbrumann/polyfill-unserialize.svg?branch=master)](https://travis-ci.org/dbrumann/polyfill-unserialize)
===
Backports unserialize options introduced in PHP 7.0 to older PHP versions.
This was originally designed as a Proof of Concept for Symfony Issue [#21090](https://github.com/symfony/symfony/pull/21090).
You can use this package in projects that rely on PHP versions older than PHP 7.0.
In case you are using PHP 7.0+ the original `unserialize()` will be used instead.
From the [documentation](https://secure.php.net/manual/en/function.unserialize.php):
> Warning: Do not pass untrusted user input to unserialize(). Unserialization can
> result in code being loaded and executed due to object instantiation
> and autoloading, and a malicious user may be able to exploit this.
This warning holds true even when `allowed_classes` is used.
Requirements
------------
- PHP 5.3+
Installation
------------
You can install this package via composer:
```
composer require brumann/polyfill-unserialize "^1.0"
```
Known Issues
------------
There is a mismatch in behavior when `allowed_classes` in `$options` is not
of the correct type (array or boolean). PHP 7.1 will issue a warning, whereas
PHP 7.0 will not. I opted to copy the behavior of the former.
Tests
-----
You can run the test suite using PHPUnit. It is intentionally not bundled as
dev dependency to make sure this package has the lowest restrictions on the
implementing system as possible.
Please read the [PHPUnit Manual](https://phpunit.de/manual/current/en/installation.html)
for information how to install it on your system.
You can run the test suite as follows:
```
phpunit -c phpunit.xml.dist tests/
```
Contributing
------------
This package is considered feature complete. As such I will likely not update it
unless there are security issues.
Should you find any bugs or have questions, feel free to submit an Issue or a Pull Request.
@@ -0,0 +1,26 @@
{
"name": "brumann/polyfill-unserialize",
"description": "Backports unserialize options introduced in PHP 7.0 to older PHP versions.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Denis Brumann",
"email": "denis.brumann@sensiolabs.de"
}
],
"autoload": {
"psr-4": {
"Brumann\\Polyfill\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Brumann\\Polyfill\\": "tests/"
}
},
"minimum-stability": "stable",
"require": {
"php": "^5.3|^7.0"
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Brumann\Polyfill Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>
@@ -0,0 +1,58 @@
<?php
namespace Brumann\Polyfill;
final class Unserialize
{
/**
* @see https://secure.php.net/manual/en/function.unserialize.php
*
* @param string $serialized Serialized data
* @param array $options Associative array containing options
*
* @return mixed
*/
public static function unserialize($serialized, array $options = array())
{
if (PHP_VERSION_ID >= 70000) {
return \unserialize($serialized, $options);
}
if (!array_key_exists('allowed_classes', $options)) {
$options['allowed_classes'] = true;
}
$allowedClasses = $options['allowed_classes'];
if (true === $allowedClasses) {
return \unserialize($serialized);
}
if (false === $allowedClasses) {
$allowedClasses = array();
}
if (!is_array($allowedClasses)) {
trigger_error(
'unserialize(): allowed_classes option should be array or boolean',
E_USER_WARNING
);
$allowedClasses = array();
}
$sanitizedSerialized = preg_replace_callback(
'/(^|;)O:\d+:"([^"]*)":(\d+):{/',
function ($match) use ($allowedClasses) {
list($completeMatch, $leftBorder, $className, $objectSize) = $match;
if (in_array($className, $allowedClasses)) {
return $completeMatch;
} else {
return sprintf(
'%sO:22:"__PHP_Incomplete_Class":%d:{s:27:"__PHP_Incomplete_Class_Name";%s',
$leftBorder,
$objectSize + 1, // size of object + 1 for added string
\serialize($className)
);
}
},
$serialized
);
return \unserialize($sanitizedSerialized);
}
}
+217 -19
View File
@@ -27,6 +27,42 @@ $.fn.init = function (selector, context, rootjQuery) {
};
$.fn.init.prototype = jquery_init.prototype;
/**
* Pre-filter Ajax requests to guard against XSS attacks.
*
* See https://github.com/jquery/jquery/issues/2432
*/
if ($.ajaxPrefilter) {
// For newer versions of jQuery, use an Ajax prefilter to prevent
// auto-executing script tags from untrusted domains. This is similar to the
// fix that is built in to jQuery 3.0 and higher.
$.ajaxPrefilter(function (s) {
if (s.crossDomain) {
s.contents.script = false;
}
});
}
else if ($.httpData) {
// For the version of jQuery that ships with Drupal core, override
// jQuery.httpData to prevent auto-detecting "script" data types from
// untrusted domains.
var jquery_httpData = $.httpData;
$.httpData = function (xhr, type, s) {
// @todo Consider backporting code from newer jQuery versions to check for
// a cross-domain request here, rather than using Drupal.urlIsLocal() to
// block scripts from all URLs that are not on the same site.
if (!type && !Drupal.urlIsLocal(s.url)) {
var content_type = xhr.getResponseHeader('content-type') || '';
if (content_type.indexOf('javascript') >= 0) {
// Default to a safe data type.
type = 'text';
}
}
return jquery_httpData.call(this, xhr, type, s);
};
$.httpData.prototype = jquery_httpData.prototype;
}
/**
* Attach all registered behaviors to a page element.
*
@@ -137,7 +173,7 @@ Drupal.detachBehaviors = function (context, settings, trigger) {
*/
Drupal.checkPlain = function (str) {
var character, regex,
replace = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
replace = { '&': '&amp;', "'": '&#39;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
str = String(str);
for (character in replace) {
if (replace.hasOwnProperty(character)) {
@@ -168,23 +204,76 @@ Drupal.checkPlain = function (str) {
Drupal.formatString = function(str, args) {
// Transform arguments before inserting them.
for (var key in args) {
switch (key.charAt(0)) {
// Escaped only.
case '@':
args[key] = Drupal.checkPlain(args[key]);
break;
// Pass-through.
case '!':
break;
// Escaped and placeholder.
case '%':
default:
args[key] = Drupal.theme('placeholder', args[key]);
break;
if (args.hasOwnProperty(key)) {
switch (key.charAt(0)) {
// Escaped only.
case '@':
args[key] = Drupal.checkPlain(args[key]);
break;
// Pass-through.
case '!':
break;
// Escaped and placeholder.
default:
args[key] = Drupal.theme('placeholder', args[key]);
break;
}
}
str = str.replace(key, args[key]);
}
return str;
return Drupal.stringReplace(str, args, null);
};
/**
* Replace substring.
*
* The longest keys will be tried first. Once a substring has been replaced,
* its new value will not be searched again.
*
* @param {String} str
* A string with placeholders.
* @param {Object} args
* Key-value pairs.
* @param {Array|null} keys
* Array of keys from the "args". Internal use only.
*
* @return {String}
* Returns the replaced string.
*/
Drupal.stringReplace = function (str, args, keys) {
if (str.length === 0) {
return str;
}
// If the array of keys is not passed then collect the keys from the args.
if (!$.isArray(keys)) {
keys = [];
for (var k in args) {
if (args.hasOwnProperty(k)) {
keys.push(k);
}
}
// Order the keys by the character length. The shortest one is the first.
keys.sort(function (a, b) { return a.length - b.length; });
}
if (keys.length === 0) {
return str;
}
// Take next longest one from the end.
var key = keys.pop();
var fragments = str.split(key);
if (keys.length) {
for (var i = 0; i < fragments.length; i++) {
// Process each fragment with a copy of remaining keys.
fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0));
}
}
return fragments.join(args[key]);
};
/**
@@ -251,7 +340,7 @@ Drupal.t = function (str, args, options) {
* A translated string.
*/
Drupal.formatPlural = function (count, singular, plural, args, options) {
var args = args || {};
args = args || {};
args['@count'] = count;
// Determine the index of the plural form.
var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1);
@@ -269,6 +358,89 @@ Drupal.formatPlural = function (count, singular, plural, args, options) {
}
};
/**
* Returns the passed in URL as an absolute URL.
*
* @param url
* The URL string to be normalized to an absolute URL.
*
* @return
* The normalized, absolute URL.
*
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
* @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
*/
Drupal.absoluteUrl = function (url) {
var urlParsingNode = document.createElement('a');
// Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8
// strings may throw an exception.
try {
url = decodeURIComponent(url);
} catch (e) {}
urlParsingNode.setAttribute('href', url);
// IE <= 7 normalizes the URL when assigned to the anchor node similar to
// the other browsers.
return urlParsingNode.cloneNode(false).href;
};
/**
* Returns true if the URL is within Drupal's base path.
*
* @param url
* The URL string to be tested.
*
* @return
* Boolean true if local.
*
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
*/
Drupal.urlIsLocal = function (url) {
// Always use browser-derived absolute URLs in the comparison, to avoid
// attempts to break out of the base path using directory traversal.
var absoluteUrl = Drupal.absoluteUrl(url);
var protocol = location.protocol;
// Consider URLs that match this site's base URL but use HTTPS instead of HTTP
// as local as well.
if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
protocol = 'https:';
}
var baseUrl = protocol + '//' + location.host + Drupal.settings.basePath.slice(0, -1);
// Decoding non-UTF-8 strings may throw an exception.
try {
absoluteUrl = decodeURIComponent(absoluteUrl);
} catch (e) {}
try {
baseUrl = decodeURIComponent(baseUrl);
} catch (e) {}
// The given URL matches the site's base URL, or has a path under the site's
// base URL.
return absoluteUrl === baseUrl || absoluteUrl.indexOf(baseUrl + '/') === 0;
};
/**
* Sanitizes a URL for use with jQuery.ajax().
*
* @param url
* The URL string to be sanitized.
*
* @return
* The sanitized URL.
*/
Drupal.sanitizeAjaxUrl = function (url) {
var regex = /\=\?(&|$)/;
while (url.match(regex)) {
url = url.replace(regex, '');
}
return url;
}
/**
* Generate the themed representation of a Drupal object.
*
@@ -347,10 +519,33 @@ Drupal.getSelection = function (element) {
return { 'start': element.selectionStart, 'end': element.selectionEnd };
};
/**
* Add a global variable which determines if the window is being unloaded.
*
* This is primarily used by Drupal.displayAjaxError().
*/
Drupal.beforeUnloadCalled = false;
$(window).bind('beforeunload pagehide', function () {
Drupal.beforeUnloadCalled = true;
});
/**
* Displays a JavaScript error from an Ajax response when appropriate to do so.
*/
Drupal.displayAjaxError = function (message) {
// Skip displaying the message if the user deliberately aborted (for example,
// by reloading the page or navigating to a different page) while the Ajax
// request was still ongoing. See, for example, the discussion at
// http://stackoverflow.com/questions/699941/handle-ajax-error-when-a-user-clicks-refresh.
if (!Drupal.beforeUnloadCalled) {
alert(message);
}
};
/**
* Build an error message from an Ajax response.
*/
Drupal.ajaxError = function (xmlhttp, uri) {
Drupal.ajaxError = function (xmlhttp, uri, customMessage) {
var statusCode, statusText, pathText, responseText, readyStateText, message;
if (xmlhttp.status) {
statusCode = "\n" + Drupal.t("An AJAX HTTP error occurred.") + "\n" + Drupal.t("HTTP Result Code: !status", {'!status': xmlhttp.status});
@@ -383,7 +578,10 @@ Drupal.ajaxError = function (xmlhttp, uri) {
// We don't need readyState except for status == 0.
readyStateText = xmlhttp.status == 0 ? ("\n" + Drupal.t("ReadyState: !readyState", {'!readyState': xmlhttp.readyState})) : "";
message = statusCode + pathText + statusText + responseText + readyStateText;
// Additional message beyond what the xmlhttp object provides.
customMessage = customMessage ? ("\n" + Drupal.t("CustomMessage: !customMessage", {'!customMessage': customMessage})) : "";
message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
return message;
};
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+112
View File
@@ -0,0 +1,112 @@
/**
* For jQuery versions less than 3.4.0, this replaces the jQuery.extend
* function with the one from jQuery 3.4.0, slightly modified (documented
* below) to be compatible with older jQuery versions and browsers.
*
* This provides the Object.prototype pollution vulnerability fix to Drupal
* installations running older jQuery versions, including the versions shipped
* with Drupal core and https://www.drupal.org/project/jquery_update.
*
* @see https://github.com/jquery/jquery/pull/4333
*/
(function (jQuery) {
// Do not override jQuery.extend() if the jQuery version is already >=3.4.0.
var versionParts = jQuery.fn.jquery.split('.');
var majorVersion = parseInt(versionParts[0]);
var minorVersion = parseInt(versionParts[1]);
var patchVersion = parseInt(versionParts[2]);
var isPreReleaseVersion = (patchVersion.toString() !== versionParts[2]);
if (
(majorVersion > 3) ||
(majorVersion === 3 && minorVersion > 4) ||
(majorVersion === 3 && minorVersion === 4 && patchVersion > 0) ||
(majorVersion === 3 && minorVersion === 4 && patchVersion === 0 && !isPreReleaseVersion)
) {
return;
}
/**
* This is almost verbatim copied from jQuery 3.4.0.
*
* Only two minor changes have been made:
* - The call to isFunction() is changed to jQuery.isFunction().
* - The two calls to Array.isArray() is changed to jQuery.isArray().
*
* The above two changes ensure compatibility with all older jQuery versions
* (1.4.4 - 3.3.1) and older browser versions (e.g., IE8).
*/
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[ 0 ] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
// Extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
if ( ( options = arguments[ i ] ) != null ) {
// Extend the base object
for ( name in options ) {
copy = options[ name ];
// Prevent Object.prototype pollution
// Prevent never-ending loop
if ( name === "__proto__" || target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
( copyIsArray = jQuery.isArray( copy ) ) ) ) {
src = target[ name ];
// Ensure proper type for the source value
if ( copyIsArray && !jQuery.isArray( src ) ) {
clone = [];
} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
clone = {};
} else {
clone = src;
}
copyIsArray = false;
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );
// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
};
})(jQuery);
@@ -0,0 +1,251 @@
/**
* For jQuery versions less than 3.5.0, this replaces the jQuery.htmlPrefilter()
* function with one that fixes these security vulnerabilities while also
* retaining the pre-3.5.0 behavior where it's safe to do so.
* - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
* - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
*
* Additionally, for jQuery versions that do not have a jQuery.htmlPrefilter()
* function (1.x prior to 1.12 and 2.x prior to 2.2), this adds it, and
* extends the functions that need to call it to do so.
*
* Drupal core's jQuery version is 1.4.4, but jQuery Update can provide a
* different version, so this covers all versions between 1.4.4 and 3.4.1.
* The GitHub links in the code comments below link to jQuery 1.5 code, because
* 1.4.4 isn't on GitHub, but the referenced code didn't change from 1.4.4 to
* 1.5.
*/
(function (jQuery) {
// Parts of this backport differ by jQuery version.
var versionParts = jQuery.fn.jquery.split('.');
var majorVersion = parseInt(versionParts[0]);
var minorVersion = parseInt(versionParts[1]);
// No backport is needed if we're already on jQuery 3.5 or higher.
if ( (majorVersion > 3) || (majorVersion === 3 && minorVersion >= 5) ) {
return;
}
// Prior to jQuery 3.5, jQuery converted XHTML-style self-closing tags to
// their XML equivalent: e.g., "<div />" to "<div></div>". This is
// problematic for several reasons, including that it's vulnerable to XSS
// attacks. However, since this was jQuery's behavior for many years, many
// Drupal modules and jQuery plugins may be relying on it. Therefore, we
// preserve that behavior, but for a limited set of tags only, that we believe
// to not be vulnerable. This is the set of HTML tags that satisfy all of the
// following conditions:
// - In DOMPurify's list of HTML tags. If an HTML tag isn't safe enough to
// appear in that list, then we don't want to mess with it here either.
// @see https://github.com/cure53/DOMPurify/blob/2.0.11/dist/purify.js#L128
// - A normal element (not a void, template, text, or foreign element).
// @see https://html.spec.whatwg.org/multipage/syntax.html#elements-2
// - An element that is still defined by the current HTML specification
// (not a deprecated element), because we do not want to rely on how
// browsers parse deprecated elements.
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
// - Not 'html', 'head', or 'body', because this pseudo-XHTML expansion is
// designed for fragments, not entire documents.
// - Not 'colgroup', because due to an idiosyncrasy of jQuery's original
// regular expression, it didn't match on colgroup, and we don't want to
// introduce a behavior change for that.
var selfClosingTagsToReplace = [
'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo',
'blockquote', 'button', 'canvas', 'caption', 'cite', 'code', 'data',
'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em',
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
'h4', 'h5', 'h6', 'header', 'hgroup', 'i', 'ins', 'kbd', 'label', 'legend',
'li', 'main', 'map', 'mark', 'menu', 'meter', 'nav', 'ol', 'optgroup',
'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt',
'ruby', 's', 'samp', 'section', 'select', 'small', 'source', 'span',
'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
'thead', 'time', 'tr', 'u', 'ul', 'var', 'video'
];
// Define regular expressions for <TAG/> and <TAG ATTRIBUTES/>. Doing this as
// two expressions makes it easier to target <a/> without also targeting
// every tag that starts with "a".
var xhtmlRegExpGroup = '(' + selfClosingTagsToReplace.join('|') + ')';
var whitespace = '[\\x20\\t\\r\\n\\f]';
var rxhtmlTagWithoutSpaceOrAttributes = new RegExp('<' + xhtmlRegExpGroup + '\\/>', 'gi');
var rxhtmlTagWithSpaceAndMaybeAttributes = new RegExp('<' + xhtmlRegExpGroup + '(' + whitespace + '[^>]*)\\/>', 'gi');
// jQuery 3.5 also fixed a vulnerability for when </select> appears within
// an <option> or <optgroup>, but it did that in local code that we can't
// backport directly. Instead, we filter such cases out. To do so, we need to
// determine when jQuery would otherwise invoke the vulnerable code, which it
// uses this regular expression to determine. The regular expression changed
// for version 3.0.0 and changed again for 3.4.0.
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4958
// @see https://github.com/jquery/jquery/blob/3.0.0/dist/jquery.js#L4584
// @see https://github.com/jquery/jquery/blob/3.4.0/dist/jquery.js#L4712
var rtagName;
if (majorVersion < 3) {
rtagName = /<([\w:]+)/;
}
else if (minorVersion < 4) {
rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]+)/i;
}
else {
rtagName = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i;
}
// The regular expression that jQuery uses to determine which self-closing
// tags to expand to open and close tags. This is vulnerable, because it
// matches all tag names except the few excluded ones. We only use this
// expression for determining vulnerability. The expression changed for
// version 3, but we only need to check for vulnerability in versions 1 and 2,
// so we use the expression from those versions.
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4957
var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
jQuery.extend({
htmlPrefilter: function (html) {
// This is how jQuery determines the first tag in the HTML.
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5521
var tag = ( rtagName.exec( html ) || [ "", "" ] )[ 1 ].toLowerCase();
// It is not valid HTML for <option> or <optgroup> to have <select> as
// either a descendant or sibling, and attempts to inject one can cause
// XSS on jQuery versions before 3.5. Since this is invalid HTML and a
// possible XSS attack, reject the entire string.
// @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11023
if ((tag === 'option' || tag === 'optgroup') && html.match(/<\/?select/i)) {
html = '';
}
// Retain jQuery's prior to 3.5 conversion of pseudo-XHTML, but for only
// the tags in the `selfClosingTagsToReplace` list defined above.
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5518
// @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11022
html = html.replace(rxhtmlTagWithoutSpaceOrAttributes, "<$1></$1>");
html = html.replace(rxhtmlTagWithSpaceAndMaybeAttributes, "<$1$2></$1>");
// Prior to jQuery 1.12 and 2.2, this function gets called (via code later
// in this file) in addition to, rather than instead of, the unsafe
// expansion of self-closing tags (including ones not in the list above).
// We can't prevent that unsafe expansion from running, so instead we
// check to make sure that it doesn't affect the DOM returned by the
// browser's parsing logic. If it does affect it, then it's vulnerable to
// XSS, so we reject the entire string.
if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
var htmlRisky = html.replace(rxhtmlTag, "<$1></$2>");
if (htmlRisky !== html) {
// Even though htmlRisky and html are different strings, they might
// represent the same HTML structure once parsed, in which case,
// htmlRisky is actually safe. We can ask the browser to parse both
// to find out, but the browser can't parse table fragments (e.g., a
// root-level "<td>"), so we need to wrap them. We just need this
// technique to work on all supported browsers; we don't need to
// copy from the specific jQuery version we're using.
// @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L4939
var wrapMap = {
thead: [ 1, "<table>", "</table>" ],
col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
};
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
// Function to wrap HTML into something that a browser can parse.
// @see https://github.com/jquery/jquery/blob/3.5.1/dist/jquery.js#L5032
var getWrappedHtml = function (html) {
var wrap = wrapMap[tag];
if (wrap) {
html = wrap[1] + html + wrap[2];
}
return html;
};
// Function to return canonical HTML after parsing it. This parses
// only; it doesn't execute scripts.
// @see https://github.com/jquery/jquery-migrate/blob/3.3.0/src/jquery/manipulation.js#L5
var getParsedHtml = function (html) {
var doc = window.document.implementation.createHTMLDocument( "" );
doc.body.innerHTML = html;
return doc.body ? doc.body.innerHTML : '';
};
// If the browser couldn't parse either one successfully, or if
// htmlRisky parses differently than html, then html is vulnerable,
// so reject it.
var htmlParsed = getParsedHtml(getWrappedHtml(html));
var htmlRiskyParsed = getParsedHtml(getWrappedHtml(htmlRisky));
if (htmlRiskyParsed === '' || htmlParsed === '' || (htmlRiskyParsed !== htmlParsed)) {
html = '';
}
}
}
return html;
}
});
// Prior to jQuery 1.12 and 2.2, jQuery.clean(), jQuery.buildFragment(), and
// jQuery.fn.html() did not call jQuery.htmlPrefilter(), so we add that.
if ( (majorVersion === 1 && minorVersion < 12) || (majorVersion === 2 && minorVersion < 2) ) {
// Filter the HTML coming into jQuery.fn.html().
var fnOriginalHtml = jQuery.fn.html;
jQuery.fn.extend({
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5147
html: function (value) {
if (typeof value === "string") {
value = jQuery.htmlPrefilter(value);
}
// .html() can be called as a setter (with an argument) or as a getter
// (without an argument), so invoke fnOriginalHtml() the same way that
// we were invoked.
return fnOriginalHtml.apply(this, arguments.length ? [value] : []);
}
});
// The regular expression that jQuery uses to determine if a string is HTML.
// Used by both clean() and buildFragment().
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L4960
var rhtml = /<|&#?\w+;/;
// Filter HTML coming into:
// - jQuery.clean() for versions prior to 1.9.
// - jQuery.buildFragment() for 1.9 and above.
//
// The looping constructs in the two functions might be essentially
// identical, but they're each expressed here in the way that most closely
// matches their original expression in jQuery, so that we filter all of
// the items and only the items that jQuery will treat as HTML strings.
if (majorVersion === 1 && minorVersion < 9) {
var originalClean = jQuery.clean;
jQuery.extend({
// @see https://github.com/jquery/jquery/blob/1.5/jquery.js#L5493
'clean': function (elems, context, fragment, scripts) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
if ( typeof elem === "string" && rhtml.test( elem ) ) {
elems[i] = elem = jQuery.htmlPrefilter(elem);
}
}
return originalClean.call(this, elems, context, fragment, scripts);
}
});
}
else {
var originalBuildFragment = jQuery.buildFragment;
jQuery.extend({
// @see https://github.com/jquery/jquery/blob/1.9.0/jquery.js#L6419
'buildFragment': function (elems, context, scripts, selection) {
var l = elems.length;
for ( var i = 0; i < l; i++ ) {
var elem = elems[i];
if (elem || elem === 0) {
if ( jQuery.type( elem ) !== "object" && rhtml.test( elem ) ) {
elems[i] = elem = jQuery.htmlPrefilter(elem);
}
}
}
return originalBuildFragment.call(this, elems, context, scripts, selection);
}
});
}
}
})(jQuery);
+6 -2
View File
@@ -373,7 +373,7 @@ states.Trigger.states = {
checked: {
'change': function () {
return this.attr('checked');
return this.is(':checked');
}
},
@@ -493,7 +493,11 @@ $(document).bind('state:disabled', function(e) {
$(document).bind('state:required', function(e) {
if (e.trigger) {
if (e.value) {
$(e.target).closest('.form-item, .form-wrapper').find('label').append('<span class="form-required">*</span>');
var $label = $(e.target).closest('.form-item, .form-wrapper').find('label');
// Avoids duplicate required markers on initialization.
if (!$label.find('.form-required').length) {
$label.append('<span class="form-required">*</span>');
}
}
else {
$(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();
+45 -10
View File
@@ -106,8 +106,10 @@ Drupal.tableDrag = function (table, tableSettings) {
// Add mouse bindings to the document. The self variable is passed along
// as event handlers do not have direct access to the tableDrag object.
$(document).bind('mousemove', function (event) { return self.dragRow(event, self); });
$(document).bind('mouseup', function (event) { return self.dropRow(event, self); });
$(document).bind('mousemove pointermove', function (event) { return self.dragRow(event, self); });
$(document).bind('mouseup pointerup', function (event) { return self.dropRow(event, self); });
$(document).bind('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
$(document).bind('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
};
/**
@@ -274,7 +276,10 @@ Drupal.tableDrag.prototype.makeDraggable = function (item) {
});
// Add the mousedown action for the handle.
handle.mousedown(function (event) {
handle.bind('mousedown touchstart pointerdown', function (event) {
if (event.originalEvent.type == "touchstart") {
event = event.originalEvent.touches[0];
}
// Create a new dragObject recording the event information.
self.dragObject = {};
self.dragObject.initMouseOffset = self.getMouseOffset(item, event);
@@ -500,7 +505,7 @@ Drupal.tableDrag.prototype.dragRow = function (event, self) {
if (self.indentEnabled) {
var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x;
// Set the number of indentations the mouse has been moved left or right.
var indentDiff = Math.round(xDiff / self.indentAmount * self.rtl);
var indentDiff = Math.round(xDiff / self.indentAmount);
// Indent the row with our estimated diff, which may be further
// restricted according to the rows around this row.
var indentChange = self.rowObject.indent(indentDiff);
@@ -575,13 +580,43 @@ Drupal.tableDrag.prototype.dropRow = function (event, self) {
* Get the mouse coordinates from the event (allowing for browser differences).
*/
Drupal.tableDrag.prototype.mouseCoords = function (event) {
if (event.pageX || event.pageY) {
return { x: event.pageX, y: event.pageY };
// Match both null and undefined, but not zero, by using != null.
// See https://stackoverflow.com/questions/2647867/how-to-determine-if-variable-is-undefined-or-null
if (event.pageX != null && event.pageY != null) {
return {x: event.pageX, y: event.pageY};
}
return {
x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
y: event.clientY + document.body.scrollTop - document.body.clientTop
};
// Complete support for pointer events was only introduced to jQuery in
// version 1.11.1; between versions 1.7 and 1.11.0 pointer events have the
// pageX and pageY properties undefined. In those cases, the properties must
// be retrieved from the event.originalEvent object instead.
if (event.originalEvent && event.originalEvent.pageX != null && event.originalEvent.pageY != null) {
return {x: event.originalEvent.pageX, y: event.originalEvent.pageY};
}
// Some old browsers do not support MouseEvent.pageX and *.pageY at all.
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY
// For those, we look at event.clientX and event.clientY instead.
if (event.clientX == null || event.clientY == null) {
// In some jQuery versions, some events created by jQuery do not have
// clientX and clientY. But the original event might have.
if (!event.originalEvent) {
throw new Error("The event has no coordinates, and no event.originalEvent.");
}
event = event.originalEvent;
if (event.clientX == null || event.clientY == null) {
throw new Error("The original event has no coordinates.");
}
}
// Copied from jQuery.event.fix() in jQuery 1.4.1.
// In newer jQuery versions, this code is in jQuery.event.mouseHooks.filter().
var doc = document.documentElement, body = document.body;
var pageX = event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
var pageY = event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
return {x: pageX, y: pageY};
};
/**
+5 -1
View File
@@ -57,10 +57,14 @@ Drupal.tableSelect = function () {
// Keep track of the last checked checkbox.
lastChecked = e.target;
});
// If all checkboxes are checked on page load, make sure the select-all one
// is checked too, otherwise keep unchecked.
updateSelectAll((checkboxes.length == $(checkboxes).filter(':checked').length));
};
Drupal.tableSelectRange = function (from, to, state) {
// We determine the looping mode based on the the order of from and to.
// We determine the looping mode based on the order of from and to.
var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
// Traverse through the sibling nodes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

@@ -0,0 +1,79 @@
<?php
namespace Drupal\Core\Security;
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Helper;
use TYPO3\PharStreamWrapper\Exception;
/**
* An alternate PharExtensionInterceptor to support phar-based CLI tools.
*
* @see \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor
*/
class PharExtensionInterceptor implements Assertable {
/**
* Determines whether phar file is allowed to execute.
*
* The phar file is allowed to execute if:
* - the base file name has a ".phar" suffix.
* - it is the CLI tool that has invoked the interceptor.
*
* @param string $path
* The path of the phar file to check.
* @param string $command
* The command being carried out.
*
* @return bool
* TRUE if the phar file is allowed to execute.
*
* @throws Exception
* Thrown when the file is not allowed to execute.
*/
public function assert($path, $command) {
if ($this->baseFileContainsPharExtension($path)) {
return TRUE;
}
throw new Exception(
sprintf(
'Unexpected file extension in "%s"',
$path
),
1535198703
);
}
/**
* Determines if a path has a .phar extension or invoked execution.
*
* @param string $path
* The path of the phar file to check.
*
* @return bool
* TRUE if the file has a .phar extension or if the execution has been
* invoked by the phar file.
*/
private function baseFileContainsPharExtension($path) {
$baseFile = Helper::determineBaseFile($path);
if ($baseFile === NULL) {
return FALSE;
}
// If the stream wrapper is registered by invoking a phar file that does
// not not have .phar extension then this should be allowed. For
// example, some CLI tools recommend removing the extension.
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// Find the last entry in the backtrace containing a 'file' key as
// sometimes the last caller is executed outside the scope of a file. For
// example, this occurs with shutdown functions.
do {
$caller = array_pop($backtrace);
} while (empty($caller['file']) && !empty($backtrace));
if (isset($caller['file']) && $baseFile === Helper::determineBaseFile($caller['file'])) {
return TRUE;
}
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
return strtolower($fileExtension) === 'phar';
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 TYPO3 project - https://typo3.org/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+221
View File
@@ -0,0 +1,221 @@
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/badges/quality-score.png?b=v2)](https://scrutinizer-ci.com/g/TYPO3/phar-stream-wrapper/?branch=v2)
[![Travis CI Build Status](https://travis-ci.org/TYPO3/phar-stream-wrapper.svg?branch=v2)](https://travis-ci.org/TYPO3/phar-stream-wrapper)
[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/q4ls5tg4w1d6sf4i/branch/v2?svg=true)](https://ci.appveyor.com/project/ohader/phar-stream-wrapper)
# PHP Phar Stream Wrapper
## Abstract & History
Based on Sam Thomas' findings concerning
[insecure deserialization in combination with obfuscation strategies](https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are)
allowing to hide Phar files inside valid image resources, the TYPO3 project
decided back then to introduce a `PharStreamWrapper` to intercept invocations
of the `phar://` stream in PHP and only allow usage for defined locations in
the file system.
Since the TYPO3 mission statement is **inspiring people to share**, we thought
it would be helpful for others to release our `PharStreamWrapper` as standalone
package to the PHP community.
The mentioned security issue was reported to TYPO3 on 10th June 2018 by Sam Thomas
and has been addressed concerning the specific attack vector and for this generic
`PharStreamWrapper` in TYPO3 versions 7.6.30 LTS, 8.7.17 LTS and 9.3.1 on 12th
July 2018.
* https://blog.secarma.co.uk/labs/near-phar-dangerous-unserialization-wherever-you-are
* https://youtu.be/GePBmsNJw6Y
* https://typo3.org/security/advisory/typo3-psa-2018-001/
* https://typo3.org/security/advisory/typo3-psa-2019-007/
* https://typo3.org/security/advisory/typo3-psa-2019-008/
## License
In general the TYPO3 core is released under the GNU General Public License version
2 or any later version (`GPL-2.0-or-later`). In order to avoid licensing issues and
incompatibilities this `PharStreamWrapper` is licenced under the MIT License. In case
you duplicate or modify source code, credits are not required but really appreciated.
## Credits
Thanks to [Alex Pott](https://github.com/alexpott), Drupal for creating
back-ports of all sources in order to provide compatibility with PHP v5.3.
## Installation
The `PharStreamWrapper` is provided as composer package `typo3/phar-stream-wrapper`
and has minimum requirements of PHP v5.3 ([`v2`](https://github.com/TYPO3/phar-stream-wrapper/tree/v2) branch) and PHP v7.0 ([`master`](https://github.com/TYPO3/phar-stream-wrapper) branch).
### Installation for PHP v7.0
```
composer require typo3/phar-stream-wrapper ^3.0
```
### Installation for PHP v5.3
```
composer require typo3/phar-stream-wrapper ^2.0
```
## Example
The following example is bundled within this package, the shown
`PharExtensionInterceptor` denies all stream wrapper invocations files
not having the `.phar` suffix. Interceptor logic has to be individual and
adjusted to according requirements.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharExtensionInterceptor())
);
if (in_array('phar', stream_get_wrappers())) {
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper');
}
```
* `PharStreamWrapper` defined as class reference will be instantiated each time
`phar://` streams shall be processed.
* `Manager` as singleton pattern being called by `PharStreamWrapper` instances
in order to retrieve individual behavior and settings.
* `Behavior` holds reference to interceptor(s) that shall assert correct/allowed
invocation of a given `$path` for a given `$command`. Interceptors implement
the interface `Assertable`. Interceptors can act individually on following
commands or handle all of them in case not defined specifically:
+ `COMMAND_DIR_OPENDIR`
+ `COMMAND_MKDIR`
+ `COMMAND_RENAME`
+ `COMMAND_RMDIR`
+ `COMMAND_STEAM_METADATA`
+ `COMMAND_STREAM_OPEN`
+ `COMMAND_UNLINK`
+ `COMMAND_URL_STAT`
## Interceptors
The following interceptor is shipped with the package and ready to use in order
to block any Phar invocation of files not having a `.phar` suffix. Besides that
individual interceptors are possible of course.
```
class PharExtensionInterceptor implements Assertable
{
/**
* Determines whether the base file name has a ".phar" suffix.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert($path, $command)
{
if ($this->baseFileContainsPharExtension($path)) {
return true;
}
throw new Exception(
sprintf(
'Unexpected file extension in "%s"',
$path
),
1535198703
);
}
/**
* @param string $path
* @return bool
*/
private function baseFileContainsPharExtension($path)
{
$baseFile = Helper::determineBaseFile($path);
if ($baseFile === null) {
return false;
}
$fileExtension = pathinfo($baseFile, PATHINFO_EXTENSION);
return strtolower($fileExtension) === 'phar';
}
}
```
### ConjunctionInterceptor
This interceptor combines multiple interceptors implementing `Assertable`.
It succeeds when all nested interceptors succeed as well (logical `AND`).
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new ConjunctionInterceptor(array(
new PharExtensionInterceptor(),
new PharMetaDataInterceptor()
)))
);
```
### PharExtensionInterceptor
This (basic) interceptor just checks whether the invoked Phar archive has
an according `.phar` file extension. Resolving symbolic links as well as
Phar internal alias resolving are considered as well.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharExtensionInterceptor())
);
```
### PharMetaDataInterceptor
This interceptor is actually checking serialized Phar meta-data against
PHP objects and would consider a Phar archive malicious in case not only
scalar values are found. A custom low-level `Phar\Reader` is used in order to
avoid using PHP's `Phar` object which would trigger the initial vulnerability.
```
$behavior = new \TYPO3\PharStreamWrapper\Behavior();
\TYPO3\PharStreamWrapper\Manager::initialize(
$behavior->withAssertion(new PharMetaDataInterceptor())
);
```
## Reader
* `Phar\Reader::__construct(string $fileName)`: Creates low-level reader for Phar archive
* `Phar\Reader::resolveContainer(): Phar\Container`: Resolves model representing Phar archive
* `Phar\Container::getStub(): Phar\Stub`: Resolves (plain PHP) stub section of Phar archive
* `Phar\Container::getManifest(): Phar\Manifest`: Resolves parsed Phar archive manifest as
documented at http://php.net/manual/en/phar.fileformat.manifestfile.php
* `Phar\Stub::getMappedAlias(): string`: Resolves internal Phar archive alias defined in stub
using `Phar::mapPhar('alias.phar')` - actually the plain PHP source is analyzed here
* `Phar\Manifest::getAlias(): string` - Resolves internal Phar archive alias defined in manifest
using `Phar::setAlias('alias.phar')`
* `Phar\Manifest::getMetaData(): string`: Resolves serialized Phar archive meta-data
* `Phar\Manifest::deserializeMetaData(): mixed`: Resolves deserialized Phar archive meta-data
containing only scalar values - in case an object is determined, an according
`Phar\DeserializationException` will be thrown
```
$reader = new Phar\Reader('example.phar');
var_dump($reader->resolveContainer()->getManifest()->deserializeMetaData());
```
## Helper
* `Helper::determineBaseFile(string $path): string`: Determines base file that can be
accessed using the regular file system. For instance the following path
`phar:///home/user/bundle.phar/content.txt` would be resolved to
`/home/user/bundle.phar`.
* `Helper::resetOpCache()`: Resets PHP's OPcache if enabled as work-around for
issues in `include()` or `require()` calls and OPcache delivering wrong
results. More details can be found in PHP's bug tracker, for instance like
https://bugs.php.net/bug.php?id=66569
## Security Contact
In case of finding additional security issues in the TYPO3 project or in this
`PharStreamWrapper` package in particular, please get in touch with the
[TYPO3 Security Team](mailto:security@typo3.org).
@@ -0,0 +1,30 @@
{
"name": "typo3/phar-stream-wrapper",
"description": "Interceptors for PHP's native phar:// stream handling",
"type": "library",
"license": "MIT",
"homepage": "https://typo3.org/",
"keywords": ["php", "phar", "stream-wrapper", "security"],
"require": {
"php": "^5.3.3|^7.0",
"ext-json": "*",
"brumann/polyfill-unserialize": "^1.0"
},
"require-dev": {
"ext-xdebug": "*",
"phpunit/phpunit": "^4.8.36"
},
"suggest": {
"ext-fileinfo": "For PHP builtin file type guessing, otherwise uses internal processing"
},
"autoload": {
"psr-4": {
"TYPO3\\PharStreamWrapper\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"TYPO3\\PharStreamWrapper\\Tests\\": "tests/"
}
}
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
interface Assertable
{
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert($path, $command);
}
@@ -0,0 +1,124 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Behavior implements Assertable
{
const COMMAND_DIR_OPENDIR = 'dir_opendir';
const COMMAND_MKDIR = 'mkdir';
const COMMAND_RENAME = 'rename';
const COMMAND_RMDIR = 'rmdir';
const COMMAND_STEAM_METADATA = 'stream_metadata';
const COMMAND_STREAM_OPEN = 'stream_open';
const COMMAND_UNLINK = 'unlink';
const COMMAND_URL_STAT = 'url_stat';
/**
* @var string[]
*/
private $availableCommands = array(
self::COMMAND_DIR_OPENDIR,
self::COMMAND_MKDIR,
self::COMMAND_RENAME,
self::COMMAND_RMDIR,
self::COMMAND_STEAM_METADATA,
self::COMMAND_STREAM_OPEN,
self::COMMAND_UNLINK,
self::COMMAND_URL_STAT,
);
/**
* @var Assertable[]
*/
private $assertions;
/**
* @param Assertable $assertable
* @return static
*/
public function withAssertion(Assertable $assertable)
{
$commands = func_get_args();
array_shift($commands);
$this->assertCommands($commands);
$commands = $commands ?: $this->availableCommands;
$target = clone $this;
foreach ($commands as $command) {
$target->assertions[$command] = $assertable;
}
return $target;
}
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert($path, $command)
{
$this->assertCommand($command);
$this->assertAssertionCompleteness();
return $this->assertions[$command]->assert($path, $command);
}
/**
* @param array $commands
*/
private function assertCommands(array $commands)
{
$unknownCommands = array_diff($commands, $this->availableCommands);
if (empty($unknownCommands)) {
return;
}
throw new \LogicException(
sprintf(
'Unknown commands: %s',
implode(', ', $unknownCommands)
),
1535189881
);
}
private function assertCommand($command)
{
if (in_array($command, $this->availableCommands, true)) {
return;
}
throw new \LogicException(
sprintf(
'Unknown command "%s"',
$command
),
1535189882
);
}
private function assertAssertionCompleteness()
{
$undefinedAssertions = array_diff(
$this->availableCommands,
array_keys($this->assertions)
);
if (empty($undefinedAssertions)) {
return;
}
throw new \LogicException(
sprintf(
'Missing assertions for commands: %s',
implode(', ', $undefinedAssertions)
),
1535189883
);
}
}
@@ -0,0 +1,37 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
interface Collectable
{
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation);
/**
* @param PharInvocation $invocation
* @param null $flags
* @return bool
*/
public function collect(PharInvocation $invocation, $flags = null);
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback($callback, $reverse = false);
}
@@ -0,0 +1,16 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Exception extends \RuntimeException
{
}
@@ -0,0 +1,199 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Helper provides low-level tools on file name resolving. However it does not
* (and should not) maintain any runtime state information. In order to resolve
* Phar archive paths according resolvers have to be used.
*
* @see \TYPO3\PharStreamWrapper\Resolvable::resolve()
*/
class Helper
{
/*
* Resets PHP's OPcache if enabled as work-around for issues in `include()`
* or `require()` calls and OPcache delivering wrong results.
*
* @see https://bugs.php.net/bug.php?id=66569
*/
public static function resetOpCache()
{
if (function_exists('opcache_reset')
&& function_exists('opcache_get_status')
) {
$status = opcache_get_status();
if (!empty($status['opcache_enabled'])) {
opcache_reset();
}
}
}
/**
* Determines base file that can be accessed using the regular file system.
* For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
* into "/home/user/bundle.phar".
*
* @param string $path
* @return string|null
*/
public static function determineBaseFile($path)
{
$parts = explode('/', static::normalizePath($path));
while (count($parts)) {
$currentPath = implode('/', $parts);
if (@is_file($currentPath) && realpath($currentPath) !== false) {
return $currentPath;
}
array_pop($parts);
}
return null;
}
/**
* @param string $path
* @return bool
*/
public static function hasPharPrefix($path)
{
return stripos($path, 'phar://') === 0;
}
/**
* @param string $path
* @return string
*/
public static function removePharPrefix($path)
{
$path = trim($path);
if (!static::hasPharPrefix($path)) {
return $path;
}
return substr($path, 7);
}
/**
* Normalizes a path, removes phar:// prefix, fixes Windows directory
* separators. Result is without trailing slash.
*
* @param string $path
* @return string
*/
public static function normalizePath($path)
{
return rtrim(
static::normalizeWindowsPath(
static::removePharPrefix($path)
),
'/'
);
}
/**
* Fixes a path for windows-backslashes and reduces double-slashes to single slashes
*
* @param string $path File path to process
* @return string
*/
public static function normalizeWindowsPath($path)
{
return str_replace('\\', '/', $path);
}
/**
* Resolves all dots, slashes and removes spaces after or before a path...
*
* @param string $path Input string
* @return string Canonical path, always without trailing slash
*/
private static function getCanonicalPath($path)
{
$path = static::normalizeWindowsPath($path);
$absolutePathPrefix = '';
if (static::isAbsolutePath($path)) {
if (static::isWindows() && strpos($path, ':/') === 1) {
$absolutePathPrefix = substr($path, 0, 3);
$path = substr($path, 3);
} else {
$path = ltrim($path, '/');
$absolutePathPrefix = '/';
}
}
$pathParts = explode('/', $path);
$pathPartsLength = count($pathParts);
for ($partCount = 0; $partCount < $pathPartsLength; $partCount++) {
// double-slashes in path: remove element
if ($pathParts[$partCount] === '') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// "." in path: remove element
if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '.') {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
// ".." in path:
if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '..') {
if ($partCount === 0) {
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
} elseif ($partCount >= 1) {
// Rremove this and previous element
array_splice($pathParts, $partCount - 1, 2);
$partCount -= 2;
$pathPartsLength -= 2;
} elseif ($absolutePathPrefix) {
// can't go higher than root dir
// simply remove this part and continue
array_splice($pathParts, $partCount, 1);
$partCount--;
$pathPartsLength--;
}
}
}
return $absolutePathPrefix . implode('/', $pathParts);
}
/**
* Checks if the $path is absolute or relative (detecting either '/' or
* 'x:/' as first part of string) and returns TRUE if so.
*
* @param string $path File path to evaluate
* @return bool
*/
private static function isAbsolutePath($path)
{
// Path starting with a / is always absolute, on every system
// On Windows also a path starting with a drive letter is absolute: X:/
return (isset($path[0]) ? $path[0] : null) === '/'
|| static::isWindows() && (
strpos($path, ':/') === 1
|| strpos($path, ':\\') === 1
);
}
/**
* @return bool
*/
private static function isWindows()
{
return stripos(PHP_OS, 'WIN') === 0;
}
}
@@ -0,0 +1,88 @@
<?php
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Exception;
class ConjunctionInterceptor implements Assertable
{
/**
* @var Assertable[]
*/
private $assertions;
public function __construct(array $assertions)
{
$this->assertAssertions($assertions);
$this->assertions = $assertions;
}
/**
* Executes assertions based on all contained assertions.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert($path, $command)
{
if ($this->invokeAssertions($path, $command)) {
return true;
}
throw new Exception(
sprintf(
'Assertion failed in "%s"',
$path
),
1539625084
);
}
/**
* @param Assertable[] $assertions
*/
private function assertAssertions(array $assertions)
{
foreach ($assertions as $assertion) {
if (!$assertion instanceof Assertable) {
throw new \InvalidArgumentException(
sprintf(
'Instance %s must implement Assertable',
get_class($assertion)
),
1539624719
);
}
}
}
/**
* @param string $path
* @param string $command
* @return bool
*/
private function invokeAssertions($path, $command)
{
try {
foreach ($this->assertions as $assertion) {
if (!$assertion->assert($path, $command)) {
return false;
}
}
} catch (Exception $exception) {
return false;
}
return true;
}
}
@@ -0,0 +1,55 @@
<?php
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Exception;
use TYPO3\PharStreamWrapper\Manager;
class PharExtensionInterceptor implements Assertable
{
/**
* Determines whether the base file name has a ".phar" suffix.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert($path, $command)
{
if ($this->baseFileContainsPharExtension($path)) {
return true;
}
throw new Exception(
sprintf(
'Unexpected file extension in "%s"',
$path
),
1535198703
);
}
/**
* @param string $path
* @return bool
*/
private function baseFileContainsPharExtension($path)
{
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
$fileExtension = pathinfo($invocation->getBaseName(), PATHINFO_EXTENSION);
return strtolower($fileExtension) === 'phar';
}
}
@@ -0,0 +1,73 @@
<?php
namespace TYPO3\PharStreamWrapper\Interceptor;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Assertable;
use TYPO3\PharStreamWrapper\Exception;
use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\Phar\DeserializationException;
use TYPO3\PharStreamWrapper\Phar\Reader;
/**
* @internal Experimental implementation of checking against serialized objects in Phar meta-data
* @internal This functionality has not been 100% pentested...
*/
class PharMetaDataInterceptor implements Assertable
{
/**
* Determines whether the according Phar archive contains
* (potential insecure) serialized objects.
*
* @param string $path
* @param string $command
* @return bool
* @throws Exception
*/
public function assert($path, $command)
{
if ($this->baseFileDoesNotHaveMetaDataIssues($path)) {
return true;
}
throw new Exception(
sprintf(
'Problematic meta-data in "%s"',
$path
),
1539632368
);
}
/**
* @param string $path
* @return bool
*/
private function baseFileDoesNotHaveMetaDataIssues($path)
{
$invocation = Manager::instance()->resolve($path);
if ($invocation === null) {
return false;
}
// directly return in case invocation was checked before
if ($invocation->getVariable(__CLASS__) === true) {
return true;
}
// otherwise analyze meta-data
try {
$reader = new Reader($invocation->getBaseName());
$reader->resolveContainer()->getManifest()->deserializeMetaData();
$invocation->setVariable(__CLASS__, true);
} catch (DeserializationException $exception) {
return false;
}
return true;
}
}
@@ -0,0 +1,135 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
use TYPO3\PharStreamWrapper\Resolver\PharInvocationCollection;
use TYPO3\PharStreamWrapper\Resolver\PharInvocationResolver;
class Manager
{
/**
* @var self
*/
private static $instance;
/**
* @var Behavior
*/
private $behavior;
/**
* @var Resolvable
*/
private $resolver;
/**
* @var Collectable
*/
private $collection;
/**
* @param Behavior $behaviour
* @param Resolvable $resolver
* @param Collectable $collection
* @return self
*/
public static function initialize(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
) {
if (self::$instance === null) {
self::$instance = new self($behaviour, $resolver, $collection);
return self::$instance;
}
throw new \LogicException(
'Manager can only be initialized once',
1535189871
);
}
/**
* @return self
*/
public static function instance()
{
if (self::$instance !== null) {
return self::$instance;
}
throw new \LogicException(
'Manager needs to be initialized first',
1535189872
);
}
/**
* @return bool
*/
public static function destroy()
{
if (self::$instance === null) {
return false;
}
self::$instance = null;
return true;
}
/**
* @param Behavior $behaviour
* @param Resolvable $resolver
* @param Collectable $collection
*/
private function __construct(
Behavior $behaviour,
Resolvable $resolver = null,
Collectable $collection = null
) {
if ($collection === null) {
$collection = new PharInvocationCollection();
}
if ($resolver === null) {
$resolver = new PharInvocationResolver();
}
$this->collection = $collection;
$this->resolver = $resolver;
$this->behavior = $behaviour;
}
/**
* @param string $path
* @param string $command
* @return bool
*/
public function assert($path, $command)
{
return $this->behavior->assert($path, $command);
}
/**
* @param string $path
* @param null|int $flags
* @return null|PharInvocation
*/
public function resolve($path, $flags = null)
{
return $this->resolver->resolve($path, $flags);
}
/**
* @return Collectable
*/
public function getCollection()
{
return $this->collection;
}
}
@@ -0,0 +1,59 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Container
{
/**
* @var Stub
*/
private $stub;
/**
* @var Manifest
*/
private $manifest;
/**
* @param Stub $stub
* @param Manifest $manifest
*/
public function __construct(Stub $stub, Manifest $manifest)
{
$this->stub = $stub;
$this->manifest = $manifest;
}
/**
* @return Stub
*/
public function getStub()
{
return $this->stub;
}
/**
* @return Manifest
*/
public function getManifest()
{
return $this->manifest;
}
/**
* @return string
*/
public function getAlias()
{
return $this->manifest->getAlias() ?: $this->stub->getMappedAlias();
}
}
@@ -0,0 +1,18 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Exception;
class DeserializationException extends Exception
{
}
@@ -0,0 +1,176 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Brumann\Polyfill\Unserialize;
class Manifest
{
/**
* @param string $content
* @return self
* @see http://php.net/manual/en/phar.fileformat.phar.php
*/
public static function fromContent($content)
{
$target = new static();
$target->manifestLength = Reader::resolveFourByteLittleEndian($content, 0);
$target->amountOfFiles = Reader::resolveFourByteLittleEndian($content, 4);
$target->flags = Reader::resolveFourByteLittleEndian($content, 10);
$target->aliasLength = Reader::resolveFourByteLittleEndian($content, 14);
$target->alias = substr($content, 18, $target->aliasLength);
$target->metaDataLength = Reader::resolveFourByteLittleEndian($content, 18 + $target->aliasLength);
$target->metaData = substr($content, 22 + $target->aliasLength, $target->metaDataLength);
$apiVersionNibbles = Reader::resolveTwoByteBigEndian($content, 8);
$target->apiVersion = implode('.', array(
($apiVersionNibbles & 0xf000) >> 12,
($apiVersionNibbles & 0x0f00) >> 8,
($apiVersionNibbles & 0x00f0) >> 4,
));
return $target;
}
/**
* @var int
*/
private $manifestLength;
/**
* @var int
*/
private $amountOfFiles;
/**
* @var string
*/
private $apiVersion;
/**
* @var int
*/
private $flags;
/**
* @var int
*/
private $aliasLength;
/**
* @var string
*/
private $alias;
/**
* @var int
*/
private $metaDataLength;
/**
* @var string
*/
private $metaData;
/**
* Avoid direct instantiation.
*/
private function __construct()
{
}
/**
* @return int
*/
public function getManifestLength()
{
return $this->manifestLength;
}
/**
* @return int
*/
public function getAmountOfFiles()
{
return $this->amountOfFiles;
}
/**
* @return string
*/
public function getApiVersion()
{
return $this->apiVersion;
}
/**
* @return int
*/
public function getFlags()
{
return $this->flags;
}
/**
* @return int
*/
public function getAliasLength()
{
return $this->aliasLength;
}
/**
* @return string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @return int
*/
public function getMetaDataLength()
{
return $this->metaDataLength;
}
/**
* @return string
*/
public function getMetaData()
{
return $this->metaData;
}
/**
* @return mixed|null
*/
public function deserializeMetaData()
{
if (empty($this->metaData)) {
return null;
}
$result = Unserialize::unserialize($this->metaData, array('allowed_classes' => false));
$serialized = json_encode($result);
if (strpos($serialized, '__PHP_Incomplete_Class_Name') !== false) {
throw new DeserializationException(
'Meta-data contains serialized object',
1539623382
);
}
return $result;
}
}
@@ -0,0 +1,254 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Reader
{
/**
* @var string
*/
private $fileName;
/**
* Mime-type in order to use zlib, bzip2 or no compression.
* In case ext-fileinfo is not present only the relevant types
* 'application/x-gzip' and 'application/x-bzip2' are assigned
* to this class property.
*
* @var string
*/
private $fileType;
/**
* @param string $fileName
*/
public function __construct($fileName)
{
if (strpos($fileName, '://') !== false) {
throw new ReaderException(
'File name must not contain stream prefix',
1539623708
);
}
$this->fileName = $fileName;
$this->fileType = $this->determineFileType();
}
/**
* @return Container
*/
public function resolveContainer()
{
$data = $this->extractData($this->resolveStream() . $this->fileName);
if ($data['stubContent'] === null) {
throw new ReaderException(
'Cannot resolve stub',
1547807881
);
}
if ($data['manifestContent'] === null || $data['manifestLength'] === null) {
throw new ReaderException(
'Cannot resolve manifest',
1547807882
);
}
if (strlen($data['manifestContent']) < $data['manifestLength']) {
throw new ReaderException(
sprintf(
'Exected manifest length %d, got %d',
strlen($data['manifestContent']),
$data['manifestLength']
),
1547807883
);
}
return new Container(
Stub::fromContent($data['stubContent']),
Manifest::fromContent($data['manifestContent'])
);
}
/**
* @param string $fileName e.g. '/path/file.phar' or 'compress.zlib:///path/file.phar'
* @return array
*/
private function extractData($fileName)
{
$stubContent = null;
$manifestContent = null;
$manifestLength = null;
$resource = fopen($fileName, 'r');
if (!is_resource($resource)) {
throw new ReaderException(
sprintf('Resource %s could not be opened', $fileName),
1547902055
);
}
while (!feof($resource)) {
$line = fgets($resource);
// stop reading file when manifest can be extracted
if ($manifestLength !== null && $manifestContent !== null && strlen($manifestContent) >= $manifestLength) {
break;
}
$manifestPosition = strpos($line, '__HALT_COMPILER();');
// first line contains start of manifest
if ($stubContent === null && $manifestContent === null && $manifestPosition !== false) {
$stubContent = substr($line, 0, $manifestPosition - 1);
$manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
$manifestLength = $this->resolveManifestLength($manifestContent);
// line contains start of stub
} elseif ($stubContent === null) {
$stubContent = $line;
// line contains start of manifest
} elseif ($manifestContent === null && $manifestPosition !== false) {
$manifestContent = preg_replace('#^.*__HALT_COMPILER\(\);(?>[ \n]\?>(?>\r\n|\n)?)?#', '', $line);
$manifestLength = $this->resolveManifestLength($manifestContent);
// manifest has been started (thus is cannot be stub anymore), add content
} elseif ($manifestContent !== null) {
$manifestContent .= $line;
$manifestLength = $this->resolveManifestLength($manifestContent);
// stub has been started (thus cannot be manifest here, yet), add content
} elseif ($stubContent !== null) {
$stubContent .= $line;
}
}
fclose($resource);
return array(
'stubContent' => $stubContent,
'manifestContent' => $manifestContent,
'manifestLength' => $manifestLength,
);
}
/**
* Resolves stream in order to handle compressed Phar archives.
*
* @return string
*/
private function resolveStream()
{
if ($this->fileType === 'application/x-gzip' || $this->fileType === 'application/gzip') {
return 'compress.zlib://';
} elseif ($this->fileType === 'application/x-bzip2') {
return 'compress.bzip2://';
}
return '';
}
/**
* @return string
*/
private function determineFileType()
{
if (class_exists('\\finfo')) {
$fileInfo = new \finfo();
return $fileInfo->file($this->fileName, FILEINFO_MIME_TYPE);
}
return $this->determineFileTypeByHeader();
}
/**
* In case ext-fileinfo is not present only the relevant types
* 'application/x-gzip' and 'application/x-bzip2' are resolved.
*
* @return string
*/
private function determineFileTypeByHeader()
{
$resource = fopen($this->fileName, 'r');
if (!is_resource($resource)) {
throw new ReaderException(
sprintf('Resource %s could not be opened', $this->fileName),
1557753055
);
}
$header = fgets($resource, 4);
fclose($resource);
$mimeType = '';
if (strpos($header, "\x42\x5a\x68") === 0) {
$mimeType = 'application/x-bzip2';
} elseif (strpos($header, "\x1f\x8b") === 0) {
$mimeType = 'application/x-gzip';
}
return $mimeType;
}
/**
* @param string $content
* @return int|null
*/
private function resolveManifestLength($content)
{
if (strlen($content) < 4) {
return null;
}
return static::resolveFourByteLittleEndian($content, 0);
}
/**
* @param string $content
* @param int $start
* @return int
*/
public static function resolveFourByteLittleEndian($content, $start)
{
$payload = substr($content, $start, 4);
if (!is_string($payload)) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614260
);
}
$value = unpack('V', $payload);
if (!isset($value[1])) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614261
);
}
return $value[1];
}
/**
* @param string $content
* @param int $start
* @return int
*/
public static function resolveTwoByteBigEndian($content, $start)
{
$payload = substr($content, $start, 2);
if (!is_string($payload)) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614263
);
}
$value = unpack('n', $payload);
if (!isset($value[1])) {
throw new ReaderException(
sprintf('Cannot resolve value at offset %d', $start),
1539614264
);
}
return $value[1];
}
}
@@ -0,0 +1,18 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Exception;
class ReaderException extends Exception
{
}
@@ -0,0 +1,65 @@
<?php
namespace TYPO3\PharStreamWrapper\Phar;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* @internal Experimental implementation of Phar archive internals
*/
class Stub
{
/**
* @param string $content
* @return self
*/
public static function fromContent($content)
{
$target = new static();
$target->content = $content;
if (
stripos($content, 'Phar::mapPhar(') !== false
&& preg_match('#Phar\:\:mapPhar\(([^)]+)\)#', $content, $matches)
) {
// remove spaces, single & double quotes
// @todo `'my' . 'alias' . '.phar'` is not evaluated here
$target->mappedAlias = trim($matches[1], ' \'"');
}
return $target;
}
/**
* @var string
*/
private $content;
/**
* @var string
*/
private $mappedAlias = '';
/**
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* @return string
*/
public function getMappedAlias()
{
return $this->mappedAlias;
}
}
@@ -0,0 +1,511 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
class PharStreamWrapper
{
/**
* Internal stream constants that are not exposed to PHP, but used...
* @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
*/
const STREAM_OPEN_FOR_INCLUDE = 128;
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
protected $internalResource;
/**
* @var PharInvocation
*/
protected $invocation;
/**
* @return bool
*/
public function dir_closedir()
{
if (!is_resource($this->internalResource)) {
return false;
}
$this->invokeInternalStreamWrapper(
'closedir',
$this->internalResource
);
return !is_resource($this->internalResource);
}
/**
* @param string $path
* @param int $options
* @return bool
*/
public function dir_opendir($path, $options)
{
$this->assert($path, Behavior::COMMAND_DIR_OPENDIR);
$this->internalResource = $this->invokeInternalStreamWrapper(
'opendir',
$path,
$this->context
);
return is_resource($this->internalResource);
}
/**
* @return string|false
*/
public function dir_readdir()
{
return $this->invokeInternalStreamWrapper(
'readdir',
$this->internalResource
);
}
/**
* @return bool
*/
public function dir_rewinddir()
{
if (!is_resource($this->internalResource)) {
return false;
}
$this->invokeInternalStreamWrapper(
'rewinddir',
$this->internalResource
);
return is_resource($this->internalResource);
}
/**
* @param string $path
* @param int $mode
* @param int $options
* @return bool
*/
public function mkdir($path, $mode, $options)
{
$this->assert($path, Behavior::COMMAND_MKDIR);
return $this->invokeInternalStreamWrapper(
'mkdir',
$path,
$mode,
(bool) ($options & STREAM_MKDIR_RECURSIVE),
$this->context
);
}
/**
* @param string $path_from
* @param string $path_to
* @return bool
*/
public function rename($path_from, $path_to)
{
$this->assert($path_from, Behavior::COMMAND_RENAME);
$this->assert($path_to, Behavior::COMMAND_RENAME);
return $this->invokeInternalStreamWrapper(
'rename',
$path_from,
$path_to,
$this->context
);
}
/**
* @param string $path
* @param int $options
* @return bool
*/
public function rmdir($path, $options)
{
$this->assert($path, Behavior::COMMAND_RMDIR);
return $this->invokeInternalStreamWrapper(
'rmdir',
$path,
$this->context
);
}
/**
* @param int $cast_as
*/
public function stream_cast($cast_as)
{
throw new Exception(
'Method stream_select() cannot be used',
1530103999
);
}
public function stream_close()
{
$this->invokeInternalStreamWrapper(
'fclose',
$this->internalResource
);
}
/**
* @return bool
*/
public function stream_eof()
{
return $this->invokeInternalStreamWrapper(
'feof',
$this->internalResource
);
}
/**
* @return bool
*/
public function stream_flush()
{
return $this->invokeInternalStreamWrapper(
'fflush',
$this->internalResource
);
}
/**
* @param int $operation
* @return bool
*/
public function stream_lock($operation)
{
return $this->invokeInternalStreamWrapper(
'flock',
$this->internalResource,
$operation
);
}
/**
* @param string $path
* @param int $option
* @param string|int $value
* @return bool
*/
public function stream_metadata($path, $option, $value)
{
$this->assert($path, Behavior::COMMAND_STEAM_METADATA);
if ($option === STREAM_META_TOUCH) {
return call_user_func_array(
array($this, 'invokeInternalStreamWrapper'),
array_merge(array('touch', $path), (array) $value)
);
}
if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) {
return $this->invokeInternalStreamWrapper(
'chown',
$path,
$value
);
}
if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) {
return $this->invokeInternalStreamWrapper(
'chgrp',
$path,
$value
);
}
if ($option === STREAM_META_ACCESS) {
return $this->invokeInternalStreamWrapper(
'chmod',
$path,
$value
);
}
return false;
}
/**
* @param string $path
* @param string $mode
* @param int $options
* @param string|null $opened_path
* @return bool
*/
public function stream_open(
$path,
$mode,
$options,
&$opened_path = null
) {
$this->assert($path, Behavior::COMMAND_STREAM_OPEN);
$arguments = array($path, $mode, (bool) ($options & STREAM_USE_PATH));
// only add stream context for non include/require calls
if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) {
$arguments[] = $this->context;
// work around https://bugs.php.net/bug.php?id=66569
// for including files from Phar stream with OPcache enabled
} else {
Helper::resetOpCache();
}
$this->internalResource = call_user_func_array(
array($this, 'invokeInternalStreamWrapper'),
array_merge(array('fopen'), $arguments)
);
if (!is_resource($this->internalResource)) {
return false;
}
if ($opened_path !== null) {
$metaData = stream_get_meta_data($this->internalResource);
$opened_path = $metaData['uri'];
}
return true;
}
/**
* @param int $count
* @return string
*/
public function stream_read($count)
{
return $this->invokeInternalStreamWrapper(
'fread',
$this->internalResource,
$count
);
}
/**
* @param int $offset
* @param int $whence
* @return bool
*/
public function stream_seek($offset, $whence = SEEK_SET)
{
return $this->invokeInternalStreamWrapper(
'fseek',
$this->internalResource,
$offset,
$whence
) !== -1;
}
/**
* @param int $option
* @param int $arg1
* @param int $arg2
* @return bool
*/
public function stream_set_option($option, $arg1, $arg2)
{
if ($option === STREAM_OPTION_BLOCKING) {
return $this->invokeInternalStreamWrapper(
'stream_set_blocking',
$this->internalResource,
$arg1
);
}
if ($option === STREAM_OPTION_READ_TIMEOUT) {
return $this->invokeInternalStreamWrapper(
'stream_set_timeout',
$this->internalResource,
$arg1,
$arg2
);
}
if ($option === STREAM_OPTION_WRITE_BUFFER) {
return $this->invokeInternalStreamWrapper(
'stream_set_write_buffer',
$this->internalResource,
$arg2
) === 0;
}
return false;
}
/**
* @return array
*/
public function stream_stat()
{
return $this->invokeInternalStreamWrapper(
'fstat',
$this->internalResource
);
}
/**
* @return int
*/
public function stream_tell()
{
return $this->invokeInternalStreamWrapper(
'ftell',
$this->internalResource
);
}
/**
* @param int $new_size
* @return bool
*/
public function stream_truncate($new_size)
{
return $this->invokeInternalStreamWrapper(
'ftruncate',
$this->internalResource,
$new_size
);
}
/**
* @param string $data
* @return int
*/
public function stream_write($data)
{
return $this->invokeInternalStreamWrapper(
'fwrite',
$this->internalResource,
$data
);
}
/**
* @param string $path
* @return bool
*/
public function unlink($path)
{
$this->assert($path, Behavior::COMMAND_UNLINK);
return $this->invokeInternalStreamWrapper(
'unlink',
$path,
$this->context
);
}
/**
* @param string $path
* @param int $flags
* @return array|false
*/
public function url_stat($path, $flags)
{
$this->assert($path, Behavior::COMMAND_URL_STAT);
$functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
return $this->invokeInternalStreamWrapper($functionName, $path);
}
/**
* @param string $path
* @param string $command
*/
protected function assert($path, $command)
{
if (Manager::instance()->assert($path, $command) === true) {
$this->collectInvocation($path);
return;
}
throw new Exception(
sprintf(
'Denied invocation of "%s" for command "%s"',
$path,
$command
),
1535189880
);
}
/**
* @param string $path
*/
protected function collectInvocation($path)
{
if (isset($this->invocation)) {
return;
}
$manager = Manager::instance();
$this->invocation = $manager->resolve($path);
if ($this->invocation === null) {
throw new Exception(
'Expected invocation could not be resolved',
1556389591
);
}
// confirm, previous interceptor(s) validated invocation
$this->invocation->confirm();
$collection = $manager->getCollection();
if (!$collection->has($this->invocation)) {
$collection->collect($this->invocation);
}
}
/**
* @return Manager|Assertable
* @deprecated Use Manager::instance() directly
*/
protected function resolveAssertable()
{
return Manager::instance();
}
/**
* Invokes commands on the native PHP Phar stream wrapper.
*
* @param string $functionName
* @param mixed ...$arguments
* @return mixed
*/
private function invokeInternalStreamWrapper($functionName)
{
$arguments = func_get_args();
array_shift($arguments);
$silentExecution = $functionName[0] === '@';
$functionName = ltrim($functionName, '@');
$this->restoreInternalSteamWrapper();
try {
if ($silentExecution) {
$result = @call_user_func_array($functionName, $arguments);
} else {
$result = call_user_func_array($functionName, $arguments);
}
} catch (\Exception $exception) {
$this->registerStreamWrapper();
throw $exception;
} catch (\Throwable $throwable) {
$this->registerStreamWrapper();
throw $throwable;
}
$this->registerStreamWrapper();
return $result;
}
private function restoreInternalSteamWrapper()
{
stream_wrapper_restore('phar');
}
private function registerStreamWrapper()
{
stream_wrapper_unregister('phar');
stream_wrapper_register('phar', get_class($this));
}
}
@@ -0,0 +1,24 @@
<?php
namespace TYPO3\PharStreamWrapper;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Resolver\PharInvocation;
interface Resolvable
{
/**
* @param string $path
* @param null|int $flags
* @return null|PharInvocation
*/
public function resolve($path, $flags = null);
}
@@ -0,0 +1,125 @@
<?php
namespace TYPO3\PharStreamWrapper\Resolver;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Exception;
class PharInvocation
{
/**
* @var string
*/
private $baseName;
/**
* @var string
*/
private $alias;
/**
* @var bool
* @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
*/
private $confirmed = false;
/**
* Arbitrary variables to be used by interceptors as registry
* (e.g. in order to avoid duplicate processing and assertions)
*
* @var array
*/
private $variables;
/**
* @param string $baseName
* @param string $alias
*/
public function __construct($baseName, $alias = '')
{
if ($baseName === '') {
throw new Exception(
'Base-name cannot be empty',
1551283689
);
}
$this->baseName = $baseName;
$this->alias = $alias;
}
/**
* @return string
*/
public function __toString()
{
return $this->baseName;
}
/**
* @return string
*/
public function getBaseName()
{
return $this->baseName;
}
/**
* @return null|string
*/
public function getAlias()
{
return $this->alias;
}
/**
* @return bool
*/
public function isConfirmed()
{
return $this->confirmed;
}
public function confirm()
{
$this->confirmed = true;
}
/**
* @param string $name
* @return mixed|null
*/
public function getVariable($name)
{
if (!isset($this->variables[$name])) {
return null;
}
return $this->variables[$name];
}
/**
* @param string $name
* @param mixed $value
*/
public function setVariable($name, $value)
{
$this->variables[$name] = $value;
}
/**
* @param PharInvocation $other
* @return bool
*/
public function equals(PharInvocation $other)
{
return $other->baseName === $this->baseName
&& $other->alias === $this->alias;
}
}
@@ -0,0 +1,156 @@
<?php
namespace TYPO3\PharStreamWrapper\Resolver;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Collectable;
class PharInvocationCollection implements Collectable
{
const UNIQUE_INVOCATION = 1;
const UNIQUE_BASE_NAME = 2;
const DUPLICATE_ALIAS_WARNING = 32;
/**
* @var PharInvocation[]
*/
private $invocations = array();
/**
* @param PharInvocation $invocation
* @return bool
*/
public function has(PharInvocation $invocation)
{
return in_array($invocation, $this->invocations, true);
}
/**
* @param PharInvocation $invocation
* @param null|int $flags
* @return bool
*/
public function collect(PharInvocation $invocation, $flags = null)
{
if ($flags === null) {
$flags = static::UNIQUE_INVOCATION | static::DUPLICATE_ALIAS_WARNING;
}
if ($invocation->getBaseName() === ''
|| $invocation->getAlias() === ''
|| !$this->assertUniqueBaseName($invocation, $flags)
|| !$this->assertUniqueInvocation($invocation, $flags)
) {
return false;
}
if ($flags & static::DUPLICATE_ALIAS_WARNING) {
$this->triggerDuplicateAliasWarning($invocation);
}
$this->invocations[] = $invocation;
return true;
}
/**
* @param callable $callback
* @param bool $reverse
* @return null|PharInvocation
*/
public function findByCallback($callback, $reverse = false)
{
foreach ($this->getInvocations($reverse) as $invocation) {
if (call_user_func($callback, $invocation) === true) {
return $invocation;
}
}
return null;
}
/**
* Asserts that base-name is unique. This disallows having multiple invocations for
* same base-name but having different alias names.
*
* @param PharInvocation $invocation
* @param int $flags
* @return bool
*/
private function assertUniqueBaseName(PharInvocation $invocation, $flags)
{
if (!($flags & static::UNIQUE_BASE_NAME)) {
return true;
}
return $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->getBaseName() === $invocation->getBaseName();
}
) === null;
}
/**
* Asserts that combination of base-name and alias is unique. This allows having multiple
* invocations for same base-name but having different alias names (for whatever reason).
*
* @param PharInvocation $invocation
* @param int $flags
* @return bool
*/
private function assertUniqueInvocation(PharInvocation $invocation, $flags)
{
if (!($flags & static::UNIQUE_INVOCATION)) {
return true;
}
return $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->equals($invocation);
}
) === null;
}
/**
* Triggers warning for invocations with same alias and same confirmation state.
*
* @param PharInvocation $invocation
* @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
*/
private function triggerDuplicateAliasWarning(PharInvocation $invocation)
{
$sameAliasInvocation = $this->findByCallback(
function (PharInvocation $candidate) use ($invocation) {
return $candidate->isConfirmed() === $invocation->isConfirmed()
&& $candidate->getAlias() === $invocation->getAlias();
},
true
);
if ($sameAliasInvocation === null) {
return;
}
trigger_error(
sprintf(
'Alias %s cannot be used by %s, already used by %s',
$invocation->getAlias(),
$invocation->getBaseName(),
$sameAliasInvocation->getBaseName()
),
E_USER_WARNING
);
}
/**
* @param bool $reverse
* @return PharInvocation[]
*/
private function getInvocations($reverse = false)
{
if ($reverse) {
return array_reverse($this->invocations);
}
return $this->invocations;
}
}
@@ -0,0 +1,249 @@
<?php
namespace TYPO3\PharStreamWrapper\Resolver;
/*
* This file is part of the TYPO3 project.
*
* It is free software; you can redistribute it and/or modify it under the terms
* of the MIT License (MIT). For the full copyright and license information,
* please read the LICENSE file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\PharStreamWrapper\Helper;
use TYPO3\PharStreamWrapper\Manager;
use TYPO3\PharStreamWrapper\Phar\Reader;
use TYPO3\PharStreamWrapper\Phar\ReaderException;
use TYPO3\PharStreamWrapper\Resolvable;
class PharInvocationResolver implements Resolvable
{
const RESOLVE_REALPATH = 1;
const RESOLVE_ALIAS = 2;
const ASSERT_INTERNAL_INVOCATION = 32;
/**
* @var string[]
*/
private $invocationFunctionNames = array(
'include',
'include_once',
'require',
'require_once'
);
/**
* Contains resolved base names in order to reduce file IO.
*
* @var string[]
*/
private $baseNames = array();
/**
* Resolves PharInvocation value object (baseName and optional alias).
*
* Phar aliases are intended to be used only inside Phar archives, however
* PharStreamWrapper needs this information exposed outside of Phar as well
* It is possible that same alias is used for different $baseName values.
* That's why PharInvocationCollection behaves like a stack when resolving
* base-name for a given alias. On the other hand it is not possible that
* one $baseName is referring to multiple aliases.
* @see https://secure.php.net/manual/en/phar.setalias.php
* @see https://secure.php.net/manual/en/phar.mapphar.php
*
* @param string $path
* @param int|null $flags
* @return null|PharInvocation
*/
public function resolve($path, $flags = null)
{
$hasPharPrefix = Helper::hasPharPrefix($path);
if ($flags === null) {
$flags = static::RESOLVE_REALPATH | static::RESOLVE_ALIAS;
}
if ($hasPharPrefix && $flags & static::RESOLVE_ALIAS) {
$invocation = $this->findByAlias($path);
if ($invocation !== null) {
return $invocation;
}
}
$baseName = $this->resolveBaseName($path, $flags);
if ($baseName === null) {
return null;
}
if ($flags & static::RESOLVE_REALPATH) {
$baseName = $this->baseNames[$baseName];
}
return $this->retrieveInvocation($baseName, $flags);
}
/**
* Retrieves PharInvocation, either existing in collection or created on demand
* with resolving a potential alias name used in the according Phar archive.
*
* @param string $baseName
* @param int $flags
* @return PharInvocation
*/
private function retrieveInvocation($baseName, $flags)
{
$invocation = $this->findByBaseName($baseName);
if ($invocation !== null) {
return $invocation;
}
if ($flags & static::RESOLVE_ALIAS) {
$reader = new Reader($baseName);
$alias = $reader->resolveContainer()->getAlias();
} else {
$alias = '';
}
// add unconfirmed(!) new invocation to collection
$invocation = new PharInvocation($baseName, $alias);
Manager::instance()->getCollection()->collect($invocation);
return $invocation;
}
/**
* @param string $path
* @param int $flags
* @return null|string
*/
private function resolveBaseName($path, $flags)
{
$baseName = $this->findInBaseNames($path);
if ($baseName !== null) {
return $baseName;
}
$baseName = Helper::determineBaseFile($path);
if ($baseName !== null) {
$this->addBaseName($baseName);
return $baseName;
}
$possibleAlias = $this->resolvePossibleAlias($path);
if (!($flags & static::RESOLVE_ALIAS) || $possibleAlias === null) {
return null;
}
$trace = debug_backtrace();
foreach ($trace as $item) {
if (!isset($item['function']) || !isset($item['args'][0])
|| !in_array($item['function'], $this->invocationFunctionNames, true)) {
continue;
}
$currentPath = $item['args'][0];
if (Helper::hasPharPrefix($currentPath)) {
continue;
}
$currentBaseName = Helper::determineBaseFile($currentPath);
if ($currentBaseName === null) {
continue;
}
// ensure the possible alias name (how we have been called initially) matches
// the resolved alias name that was retrieved by the current possible base name
try {
$reader = new Reader($currentBaseName);
$currentAlias = $reader->resolveContainer()->getAlias();
} catch (ReaderException $exception) {
// most probably that was not a Phar file
continue;
}
if (empty($currentAlias) || $currentAlias !== $possibleAlias) {
continue;
}
$this->addBaseName($currentBaseName);
return $currentBaseName;
}
return null;
}
/**
* @param string $path
* @return null|string
*/
private function resolvePossibleAlias($path)
{
$normalizedPath = Helper::normalizePath($path);
return strstr($normalizedPath, '/', true) ?: null;
}
/**
* @param string $baseName
* @return null|PharInvocation
*/
private function findByBaseName($baseName)
{
return Manager::instance()->getCollection()->findByCallback(
function (PharInvocation $candidate) use ($baseName) {
return $candidate->getBaseName() === $baseName;
},
true
);
}
/**
* @param string $path
* @return null|string
*/
private function findInBaseNames($path)
{
// return directly if the resolved base name was submitted
if (in_array($path, $this->baseNames, true)) {
return $path;
}
$parts = explode('/', Helper::normalizePath($path));
while (count($parts)) {
$currentPath = implode('/', $parts);
if (isset($this->baseNames[$currentPath])) {
return $currentPath;
}
array_pop($parts);
}
return null;
}
/**
* @param string $baseName
*/
private function addBaseName($baseName)
{
if (isset($this->baseNames[$baseName])) {
return;
}
$this->baseNames[$baseName] = Helper::normalizeWindowsPath(
realpath($baseName)
);
}
/**
* Finds confirmed(!) invocations by alias.
*
* @param string $path
* @return null|PharInvocation
* @see \TYPO3\PharStreamWrapper\PharStreamWrapper::collectInvocation()
*/
private function findByAlias($path)
{
$possibleAlias = $this->resolvePossibleAlias($path);
if ($possibleAlias === null) {
return null;
}
return Manager::instance()->getCollection()->findByCallback(
function (PharInvocation $candidate) use ($possibleAlias) {
return $candidate->isConfirmed() && $candidate->getAlias() === $possibleAlias;
},
true
);
}
}
+6
View File
@@ -134,6 +134,8 @@ Drupal.verticalTab.prototype = {
tabShow: function () {
// Display the tab.
this.item.show();
// Show the vertical tabs.
this.item.closest('.vertical-tabs').show();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
@@ -164,6 +166,10 @@ Drupal.verticalTab.prototype = {
if ($firstTab.length) {
$firstTab.data('verticalTab').focus();
}
// Hide the vertical tabs (if no tabs remain).
else {
this.item.closest('.vertical-tabs').hide();
}
return this;
}
};