|
@@ -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; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|