Browse Source

Merge branch 'master' of https://github.com/mpetroff/pannellum into master-build

pull/952/head
strarsis 4 years ago
parent
commit
54a8dc65f8
9 changed files with 149 additions and 50 deletions
  1. +1
    -1
      VERSION
  2. +31
    -0
      changelog.md
  3. +2
    -1
      doc/json-config-parameters.md
  4. +1
    -1
      package.json
  5. BIN
      paper/paper.pdf
  6. +0
    -1
      src/css/pannellum.css
  7. +4
    -3
      src/js/libpannellum.js
  8. +108
    -43
      src/js/pannellum.js
  9. +2
    -0
      utils/multires/generate.py

+ 1
- 1
VERSION View File

@@ -1 +1 @@
2.5.3
2.5.6

+ 31
- 0
changelog.md View File

@@ -2,6 +2,37 @@ Changelog
========= =========




Changes in Pannellum 2.5.6 (2019-11-26)
---------------------------------------

Security fixes:
- Extended partial fix in v2.5.5 for XSS vulnerability that allowed script
execution when hot spots were clicked (CVE-2019-16763)


Changes in Pannellum 2.5.5 (2019-11-21)
---------------------------------------

Bugfixes:
- Fixed device orientation permission request such that it works with iOS 13
- Extend yaw bounds range to allow restricted range that crosses +/-180 deg

Security fixes:
- Fixed XSS vulnerability that allowed script execution when hot spots
were clicked (CVE-2019-16763)


Changes in Pannellum 2.5.4 (2019-09-10)
---------------------------------------

Bugfixes:
- Fixed issue with loading large equirectangular panoramas on iOS
- Fixed issue with touch-based scrolling with `draggable` set to `false`

Improvements:
- Started requesting device orientation permission (untested)


Changes in Pannellum 2.5.3 (2019-08-21) Changes in Pannellum 2.5.3 (2019-08-21)
--------------------------------------- ---------------------------------------




+ 2
- 1
doc/json-config-parameters.md View File

@@ -81,7 +81,8 @@ a link and visit this URL if Pannellum fails to work.


If set to `true`, device orientation control will be used when the panorama is If set to `true`, device orientation control will be used when the panorama is
loaded, if the device supports it. If false, device orientation control needs loaded, if the device supports it. If false, device orientation control needs
to be activated by pressing a button. Defaults to `false`.
to be activated by pressing a button. Defaults to `false`. Note that a secure
HTTPS connection is required for device orientation access in most browsers.




### `showZoomCtrl` (boolean) ### `showZoomCtrl` (boolean)


+ 1
- 1
package.json View File

@@ -1,7 +1,7 @@
{ {
"name": "pannellum", "name": "pannellum",
"description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.", "description": "Pannellum is a lightweight, free, and open source panorama viewer for the web.",
"version": "2.5.3",
"version": "2.5.6",
"bugs": { "bugs": {
"url": "https://github.com/mpetroff/pannellum/issues" "url": "https://github.com/mpetroff/pannellum/issues"
}, },


BIN
paper/paper.pdf View File


+ 0
- 1
src/css/pannellum.css View File

@@ -17,7 +17,6 @@
outline: 0; outline: 0;
line-height: 1.4; line-height: 1.4;
contain: content; contain: content;
touch-action: none;
} }


.pnlm-container * { .pnlm-container * {


+ 4
- 3
src/js/libpannellum.js View File

@@ -435,7 +435,7 @@ function Renderer(container) {


// Draw image on canvas // Draw image on canvas
var cropCanvas = document.createElement('canvas'); var cropCanvas = document.createElement('canvas');
cropCanvas.width = image.width;
cropCanvas.width = image.width / 2;
cropCanvas.height = image.height; cropCanvas.height = image.height;
var cropContext = cropCanvas.getContext('2d'); var cropContext = cropCanvas.getContext('2d');
cropContext.drawImage(image, 0, 0); cropContext.drawImage(image, 0, 0);
@@ -451,7 +451,8 @@ function Renderer(container) {
gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 1); gl.uniform1i(gl.getUniformLocation(program, 'u_image1'), 1);


// Upload second half of image to the texture // Upload second half of image to the texture
cropImage = cropContext.getImageData(image.width / 2, 0, image.width / 2, image.height);
cropContext.drawImage(image, -image.width / 2, 0);
cropImage = cropContext.getImageData(0, 0, image.width / 2, image.height);
gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage); gl.texImage2D(glBindType, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, cropImage);


// Set parameters for rendering any size // Set parameters for rendering any size
@@ -460,7 +461,7 @@ function Renderer(container) {
gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(glBindType, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(glBindType, gl.TEXTURE_MAG_FILTER, gl.LINEAR);


// Reactive first texture unit
// Reactivate first texture unit
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
} }
} }


+ 108
- 43
src/js/pannellum.js View File

@@ -254,22 +254,15 @@ controls.orientation.addEventListener('mousedown', function(e) {e.stopPropagatio
controls.orientation.addEventListener('touchstart', function(e) {e.stopPropagation();}); controls.orientation.addEventListener('touchstart', function(e) {e.stopPropagation();});
controls.orientation.addEventListener('pointerdown', function(e) {e.stopPropagation();}); controls.orientation.addEventListener('pointerdown', function(e) {e.stopPropagation();});
controls.orientation.className = 'pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control'; controls.orientation.className = 'pnlm-orientation-button pnlm-orientation-button-inactive pnlm-sprite pnlm-controls pnlm-control';
var orientationSupport, startOrientationIfSupported = false;
function deviceOrientationTest(e) {
window.removeEventListener('deviceorientation', deviceOrientationTest);
if (e && e.alpha !== null && e.beta !== null && e.gamma !== null) {
controls.container.appendChild(controls.orientation);
orientationSupport = true;
if (startOrientationIfSupported)
startOrientation();
} else {
orientationSupport = false;
}
}
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', deviceOrientationTest);
} else {
orientationSupport = false;
var orientationSupport = false;
if (window.DeviceOrientationEvent && location.protocol == 'https:' &&
navigator.userAgent.toLowerCase().indexOf('mobi') >= 0) {
// This user agent check is here because there's no way to check if a
// device has an inertia measurement unit. We used to be able to check if a
// DeviceOrientationEvent had non-null values, but with iOS 13 requiring a
// permission prompt to access such events, this is no longer possible.
controls.container.appendChild(controls.orientation);
orientationSupport = true;
} }


// Compass // Compass
@@ -402,7 +395,7 @@ function init() {
anError(config.strings.fileAccessError.replace('%s', a.outerHTML)); anError(config.strings.fileAccessError.replace('%s', a.outerHTML));
} }
var img = this.response; var img = this.response;
parseGPanoXMP(img);
parseGPanoXMP(img, p);
infoDisplay.load.msg.innerHTML = ''; infoDisplay.load.msg.innerHTML = '';
}; };
xhr.onprogress = function(e) { xhr.onprogress = function(e) {
@@ -528,7 +521,7 @@ function onImageLoad() {
* @private * @private
* @param {Image} image - Image to read XMP metadata from. * @param {Image} image - Image to read XMP metadata from.
*/ */
function parseGPanoXMP(image) {
function parseGPanoXMP(image, url) {
var reader = new FileReader(); var reader = new FileReader();
reader.addEventListener('loadend', function() { reader.addEventListener('loadend', function() {
var img = reader.result; var img = reader.result;
@@ -604,6 +597,35 @@ function parseGPanoXMP(image) {
// Load panorama // Load panorama
panoImage.src = window.URL.createObjectURL(image); panoImage.src = window.URL.createObjectURL(image);
panoImage.onerror = function() {
// If the image fails to load, we check the Content Security Policy
// headers and see if they block loading images as blobs. If they
// do, we load the image directly from the URL. While this should
// allow the image to load, it does prevent parsing of XMP data.
function getCspHeaders() {
if (!window.fetch)
return null;
return window.fetch(document.location.href)
.then(function(resp){
return resp.headers.get('Content-Security-Policy');
});
}
getCspHeaders().then(function(cspHeaders) {
if (cspHeaders) {
var invalidImgSource = cspHeaders.split(";").find(function(p) {
var matchstring = p.match(/img-src(.*)/);
if (matchstring) {
return !matchstring[1].includes("blob");
}
});
if (invalidImgSource) {
console.log('CSP blocks blobs; reverting to URL.');
panoImage.crossOrigin = config.crossOrigin;
panoImage.src = url;
}
}
});
}
}); });
if (reader.readAsBinaryString !== undefined) if (reader.readAsBinaryString !== undefined)
reader.readAsBinaryString(image); reader.readAsBinaryString(image);
@@ -933,6 +955,9 @@ var pointerIDs = [],
*/ */
function onDocumentPointerDown(event) { function onDocumentPointerDown(event) {
if (event.pointerType == 'touch') { if (event.pointerType == 'touch') {
// Only do something if the panorama is loaded
if (!loaded || !config.draggable)
return;
pointerIDs.push(event.pointerId); pointerIDs.push(event.pointerId);
pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY}); pointerCoordinates.push({clientX: event.clientX, clientY: event.clientY});
event.targetTouches = pointerCoordinates; event.targetTouches = pointerCoordinates;
@@ -948,6 +973,8 @@ function onDocumentPointerDown(event) {
*/ */
function onDocumentPointerMove(event) { function onDocumentPointerMove(event) {
if (event.pointerType == 'touch') { if (event.pointerType == 'touch') {
if (!config.draggable)
return;
for (var i = 0; i < pointerIDs.length; i++) { for (var i = 0; i < pointerIDs.length; i++) {
if (event.pointerId == pointerIDs[i]) { if (event.pointerId == pointerIDs[i]) {
pointerCoordinates[i].clientX = event.clientX; pointerCoordinates[i].clientX = event.clientX;
@@ -1416,9 +1443,9 @@ function render() {


if (config.autoRotate !== false) { if (config.autoRotate !== false) {
// When auto-rotating this check needs to happen first (see issue #764) // When auto-rotating this check needs to happen first (see issue #764)
if (config.yaw > 180) {
if (config.yaw > 360) {
config.yaw -= 360; config.yaw -= 360;
} else if (config.yaw < -180) {
} else if (config.yaw < -360) {
config.yaw += 360; config.yaw += 360;
} }
} }
@@ -1459,9 +1486,9 @@ function render() {
if (!(config.autoRotate !== false)) { if (!(config.autoRotate !== false)) {
// When not auto-rotating, this check needs to happen after the // When not auto-rotating, this check needs to happen after the
// previous check (see issue #698) // previous check (see issue #698)
if (config.yaw > 180) {
if (config.yaw > 360) {
config.yaw -= 360; config.yaw -= 360;
} else if (config.yaw < -180) {
} else if (config.yaw < -360) {
config.yaw += 360; config.yaw += 360;
} }
} }
@@ -1590,8 +1617,6 @@ function computeQuaternion(alpha, beta, gamma) {
* @param {DeviceOrientationEvent} event - Device orientation event. * @param {DeviceOrientationEvent} event - Device orientation event.
*/ */
function orientationListener(e) { function orientationListener(e) {
if (e.hasOwnProperty('requestPermission'))
e.requestPermission()
var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles(); var q = computeQuaternion(e.alpha, e.beta, e.gamma).toEulerAngles();
if (typeof(orientation) == 'number' && orientation < 10) { if (typeof(orientation) == 'number' && orientation < 10) {
// This kludge is necessary because iOS sometimes provides a few stale // This kludge is necessary because iOS sometimes provides a few stale
@@ -1723,7 +1748,7 @@ function createHotSpot(hs) {
if (config.basePath && !absoluteURL(imgp)) if (config.basePath && !absoluteURL(imgp))
imgp = config.basePath + imgp; imgp = config.basePath + imgp;
a = document.createElement('a'); a = document.createElement('a');
a.href = sanitizeURL(hs.URL ? hs.URL : imgp);
a.href = sanitizeURL(hs.URL ? hs.URL : imgp, true);
a.target = '_blank'; a.target = '_blank';
span.appendChild(a); span.appendChild(a);
var image = document.createElement('img'); var image = document.createElement('img');
@@ -1735,7 +1760,7 @@ function createHotSpot(hs) {
span.style.maxWidth = 'initial'; span.style.maxWidth = 'initial';
} else if (hs.URL) { } else if (hs.URL) {
a = document.createElement('a'); a = document.createElement('a');
a.href = sanitizeURL(hs.URL);
a.href = sanitizeURL(hs.URL, true);
if (hs.attributes) { if (hs.attributes) {
for (var key in hs.attributes) { for (var key in hs.attributes) {
a.setAttribute(key, hs.attributes[key]); a.setAttribute(key, hs.attributes[key]);
@@ -2009,7 +2034,7 @@ function processOptions(isPreview) {
var authorText = escapeHTML(config[key]); var authorText = escapeHTML(config[key]);
if (config.authorURL) { if (config.authorURL) {
var authorLink = document.createElement('a'); var authorLink = document.createElement('a');
authorLink.href = sanitizeURL(config['authorURL']);
authorLink.href = sanitizeURL(config['authorURL'], true);
authorLink.target = '_blank'; authorLink.target = '_blank';
authorLink.innerHTML = escapeHTML(config[key]); authorLink.innerHTML = escapeHTML(config[key]);
authorText = authorLink.outerHTML; authorText = authorLink.outerHTML;
@@ -2020,7 +2045,7 @@ function processOptions(isPreview) {
case 'fallback': case 'fallback':
var link = document.createElement('a'); var link = document.createElement('a');
link.href = sanitizeURL(config[key]);
link.href = sanitizeURL(config[key], true);
link.target = '_blank'; link.target = '_blank';
link.textContent = 'Click here to view this panorama in an alternative viewer.'; link.textContent = 'Click here to view this panorama in an alternative viewer.';
var message = document.createElement('p'); var message = document.createElement('p');
@@ -2084,12 +2109,8 @@ function processOptions(isPreview) {
break; break;


case 'orientationOnByDefault': case 'orientationOnByDefault':
if (config[key]) {
if (orientationSupport === undefined)
startOrientationIfSupported = true;
else if (orientationSupport === true)
startOrientation();
}
if (config[key])
startOrientation();
break; break;
} }
} }
@@ -2348,9 +2369,19 @@ function stopOrientation() {
* @private * @private
*/ */
function startOrientation() { function startOrientation() {
orientation = 1;
window.addEventListener('deviceorientation', orientationListener);
controls.orientation.classList.add('pnlm-orientation-button-active');
if (typeof DeviceMotionEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(function(response) {
if (response == 'granted') {
orientation = 1;
window.addEventListener('deviceorientation', orientationListener);
controls.orientation.classList.add('pnlm-orientation-button-active');
}
});
} else {
orientation = 1;
window.addEventListener('deviceorientation', orientationListener);
controls.orientation.classList.add('pnlm-orientation-button-active');
}
} }


/** /**
@@ -2376,16 +2407,50 @@ function escapeHTML(s) {
* The URL cannot be of protocol 'javascript'. * The URL cannot be of protocol 'javascript'.
* @private * @private
* @param {string} url - URL to sanitize * @param {string} url - URL to sanitize
* @param {boolean} href - True if URL is for link (blocks data URIs)
* @returns {string} Sanitized URL * @returns {string} Sanitized URL
*/ */
function sanitizeURL(url) {
if (url.trim().toLowerCase().indexOf('javascript:') === 0) {
function sanitizeURL(url, href) {
try {
var decoded_url = decodeURIComponent(unescape(url)).replace(/[^\w:]/g, '').toLowerCase();
} catch (e) {
return 'about:blank';
}
if (decoded_url.indexOf('javascript:') === 0 ||
decoded_url.indexOf('vbscript:') === 0) {
console.log('Script URL removed.');
return 'about:blank';
}
if (href && decoded_url.indexOf('data:') === 0) {
console.log('Data URI removed from link.');
return 'about:blank'; return 'about:blank';
} }
return url; return url;
} }


/** /**
* Unescapes HTML entities.
* Copied from Marked.js 0.7.0.
* @private
* @param {string} url - URL to sanitize
* @param {boolean} href - True if URL is for link (blocks data URIs)
* @returns {string} Sanitized URL
*/
function unescape(html) {
// Explicitly match decimal, hex, and named HTML entities
return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) {
n = n.toLowerCase();
if (n === 'colon') return ':';
if (n.charAt(0) === '#') {
return n.charAt(1) === 'x'
? String.fromCharCode(parseInt(n.substring(2), 16))
: String.fromCharCode(+n.substring(1));
}
return '';
});
}

/**
* Removes possibility of XSS atacks with URLs for CSS. * Removes possibility of XSS atacks with URLs for CSS.
* The URL will be sanitized with `sanitizeURL()` and single quotes * The URL will be sanitized with `sanitizeURL()` and single quotes
* and double quotes escaped. * and double quotes escaped.
@@ -2483,7 +2548,7 @@ this.setPitchBounds = function(bounds) {
* @returns {number} Yaw in degrees * @returns {number} Yaw in degrees
*/ */
this.getYaw = function() { this.getYaw = function() {
return config.yaw;
return (config.yaw + 540) % 360 - 180;
}; };


/** /**
@@ -2538,15 +2603,15 @@ this.getYawBounds = function() {
}; };


/** /**
* Set the minimum and maximum allowed yaws (in degrees [-180, 180]).
* Set the minimum and maximum allowed yaws (in degrees [-360, 360]).
* @memberof Viewer * @memberof Viewer
* @instance * @instance
* @param {number[]} bounds - [minimum yaw, maximum yaw] * @param {number[]} bounds - [minimum yaw, maximum yaw]
* @returns {Viewer} `this` * @returns {Viewer} `this`
*/ */
this.setYawBounds = function(bounds) { this.setYawBounds = function(bounds) {
config.minYaw = Math.max(-180, Math.min(bounds[0], 180));
config.maxYaw = Math.max(-180, Math.min(bounds[1], 180));
config.minYaw = Math.max(-360, Math.min(bounds[0], 360));
config.maxYaw = Math.max(-360, Math.min(bounds[1], 360));
return this; return this;
}; };




+ 2
- 0
utils/multires/generate.py View File

@@ -132,6 +132,8 @@ else:
cubeSize = 8 * int((360 / haov) * origWidth / math.pi / 8) cubeSize = 8 * int((360 / haov) * origWidth / math.pi / 8)
tileSize = min(args.tileSize, cubeSize) tileSize = min(args.tileSize, cubeSize)
levels = int(math.ceil(math.log(float(cubeSize) / tileSize, 2))) + 1 levels = int(math.ceil(math.log(float(cubeSize) / tileSize, 2))) + 1
if round(cubeSize / 2**(levels - 2)) == tileSize:
levels -= 1 # Handle edge case
origHeight = str(origHeight) origHeight = str(origHeight)
origWidth = str(origWidth) origWidth = str(origWidth)
origFilename = os.path.join(os.getcwd(), args.inputFile) origFilename = os.path.join(os.getcwd(), args.inputFile)


Loading…
Cancel
Save