-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New: Watermarking preferences #721
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { openUrlInsideIframe } from './util'; | ||
|
||
const DEFAULT_DOWNLOAD_HOST_PREFIX = 'https://dl.'; | ||
const PROD_CUSTOM_HOST_SUFFIX = 'boxcloud.com'; | ||
const DOWNLOAD_NOTIFICATION_SHOWN_KEY = 'download_host_notification_shown'; | ||
const DOWNLOAD_HOST_FALLBACK_KEY = 'download_host_fallback'; | ||
const NUMBERED_HOST_PREFIX_REGEX = /^https:\/\/dl\d+\./; | ||
const CUSTOM_HOST_PREFIX_REGEX = /^https:\/\/[A-Za-z0-9]+./; | ||
|
||
class DownloadReachability { | ||
/** | ||
* Extracts the hostname from a URL | ||
* | ||
* @param {string} downloadUrl - Content download URL, may either be a template or an actual URL | ||
* @return {string} The hoostname of the given URL | ||
*/ | ||
static getHostnameFromUrl(downloadUrl) { | ||
const contentHost = document.createElement('a'); | ||
contentHost.href = downloadUrl; | ||
return contentHost.hostname; | ||
} | ||
|
||
/** | ||
* Checks if the url is a download host, but not the default download host. | ||
* | ||
* @public | ||
* @param {string} downloadUrl - Content download URL, may either be a template or an actual URL | ||
* @return {boolean} - HTTP response | ||
*/ | ||
static isCustomDownloadHost(downloadUrl) { | ||
// A custom download host either | ||
// 1. begins with a numbered dl hostname | ||
// 2. or starts with a custom prefix and ends with boxcloud.com | ||
return ( | ||
!downloadUrl.startsWith(DEFAULT_DOWNLOAD_HOST_PREFIX) && | ||
(!!downloadUrl.match(NUMBERED_HOST_PREFIX_REGEX) || downloadUrl.indexOf(PROD_CUSTOM_HOST_SUFFIX) !== -1) | ||
); | ||
} | ||
|
||
/** | ||
* Replaces the hostname of a download URL with the default hostname, https://dl. | ||
* | ||
* @public | ||
* @param {string} downloadUrl - Content download URL, may either be a template or an actual URL | ||
* @return {string} - The updated download URL | ||
*/ | ||
static replaceDownloadHostWithDefault(downloadUrl) { | ||
if (downloadUrl.match(NUMBERED_HOST_PREFIX_REGEX)) { | ||
// First check to see if we can swap a numbered dl prefix for the default | ||
return downloadUrl.replace(NUMBERED_HOST_PREFIX_REGEX, DEFAULT_DOWNLOAD_HOST_PREFIX); | ||
} | ||
|
||
// Otherwise replace the custom prefix with the default | ||
return downloadUrl.replace(CUSTOM_HOST_PREFIX_REGEX, DEFAULT_DOWNLOAD_HOST_PREFIX); | ||
} | ||
|
||
/** | ||
* Sets session storage to use the default download host. | ||
* | ||
* @public | ||
* @return {void} | ||
*/ | ||
static setDownloadHostFallback() { | ||
sessionStorage.setItem(DOWNLOAD_HOST_FALLBACK_KEY, 'true'); | ||
} | ||
|
||
/** | ||
* Checks if we have detected a blocked download host and have decided to fall back. | ||
* | ||
* @public | ||
* @return {boolean} Whether the sessionStorage indicates that a download host has been blocked | ||
*/ | ||
static isDownloadHostBlocked() { | ||
return sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY) === 'true'; | ||
} | ||
|
||
/** | ||
* Stores the host in an array via localstorage so that we don't show a notification for it again | ||
* | ||
* @public | ||
* @param {string} downloadHost - Download URL host name | ||
* @return {void} | ||
*/ | ||
static setDownloadHostNotificationShown(downloadHost) { | ||
const shownHostsArr = JSON.parse(localStorage.getItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY)) || []; | ||
shownHostsArr.push(downloadHost); | ||
localStorage.setItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY, JSON.stringify(shownHostsArr)); | ||
} | ||
|
||
/** | ||
* Determines what notification should be shown if needed. | ||
* | ||
* @public | ||
* @param {string} downloadUrl - Content download URL | ||
* @return {string|undefined} Which host should we show a notification for, if any | ||
*/ | ||
static getDownloadNotificationToShow(downloadUrl) { | ||
const contentHostname = DownloadReachability.getHostnameFromUrl(downloadUrl); | ||
const shownHostsArr = JSON.parse(localStorage.getItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY)) || []; | ||
|
||
return sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY) === 'true' && | ||
!shownHostsArr.includes(contentHostname) && | ||
DownloadReachability.isCustomDownloadHost(downloadUrl) | ||
? contentHostname | ||
: undefined; | ||
} | ||
|
||
/** | ||
* Checks if the provided host is reachable. If not set the session storage to reflect this. | ||
* | ||
* @param {string} downloadUrl - Content download URL, may either be a template or an actual URL | ||
* @return {void} | ||
*/ | ||
static setDownloadReachability(downloadUrl) { | ||
return fetch(downloadUrl, { method: 'HEAD' }) | ||
.then(() => { | ||
return Promise.resolve(false); | ||
}) | ||
.catch(() => { | ||
DownloadReachability.setDownloadHostFallback(); | ||
return Promise.resolve(true); | ||
}); | ||
} | ||
|
||
/** | ||
* Downloads file with reachability checks. | ||
* | ||
* @param {string} downloadUrl - Content download URL | ||
* @return {void} | ||
*/ | ||
static downloadWithReachabilityCheck(downloadUrl) { | ||
const defaultDownloadUrl = DownloadReachability.replaceDownloadHostWithDefault(downloadUrl); | ||
if (DownloadReachability.isDownloadHostBlocked() || !DownloadReachability.isCustomDownloadHost(downloadUrl)) { | ||
// If we know the host is blocked, or we are already using the default, | ||
// use the default. | ||
openUrlInsideIframe(defaultDownloadUrl); | ||
} else { | ||
// Try the custom host, then check reachability | ||
openUrlInsideIframe(downloadUrl); | ||
DownloadReachability.setDownloadReachability(downloadUrl).then((isBlocked) => { | ||
if (isBlocked) { | ||
// If download is unreachable, try again with default | ||
openUrlInsideIframe(defaultDownloadUrl); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
|
||
export default DownloadReachability; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,26 +13,21 @@ import PreviewErrorViewer from './viewers/error/PreviewErrorViewer'; | |
import PreviewUI from './PreviewUI'; | ||
import getTokens from './tokens'; | ||
import Timer from './Timer'; | ||
import DownloadReachability from './DownloadReachability'; | ||
import { | ||
get, | ||
getProp, | ||
post, | ||
decodeKeydown, | ||
openUrlInsideIframe, | ||
getHeaders, | ||
findScriptLocation, | ||
appendQueryParams, | ||
replacePlaceholders, | ||
stripAuthFromString, | ||
isValidFileId, | ||
isBoxWebApp | ||
isBoxWebApp, | ||
convertWatermarkPref | ||
} from './util'; | ||
import { | ||
isDownloadHostBlocked, | ||
setDownloadReachability, | ||
isCustomDownloadHost, | ||
replaceDownloadHostWithDefault | ||
} from './downloadReachability'; | ||
import { | ||
getURL, | ||
getDownloadURL, | ||
|
@@ -44,7 +39,8 @@ import { | |
isWatermarked, | ||
getCachedFile, | ||
normalizeFileVersion, | ||
canDownload | ||
canDownload, | ||
shouldDownloadWM | ||
} from './file'; | ||
import { | ||
API_HOST, | ||
|
@@ -488,31 +484,39 @@ class Preview extends EventEmitter { | |
* @return {void} | ||
*/ | ||
download() { | ||
const { apiHost, queryParams } = this.options; | ||
|
||
const downloadErrorMsg = __('notification_cannot_download'); | ||
if (!canDownload(this.file, this.options)) { | ||
this.ui.showNotification(downloadErrorMsg); | ||
return; | ||
} | ||
|
||
// Append optional query params | ||
const downloadUrl = appendQueryParams(getDownloadURL(this.file.id, apiHost), queryParams); | ||
get(downloadUrl, this.getRequestHeaders()).then((data) => { | ||
const defaultDownloadUrl = replaceDownloadHostWithDefault(data.download_url); | ||
if (isDownloadHostBlocked() || !isCustomDownloadHost(data.download_url)) { | ||
// If we know the host is blocked, or we are already using the default, | ||
// use the default. | ||
openUrlInsideIframe(defaultDownloadUrl); | ||
} else { | ||
// Try the custom host, then check reachability | ||
openUrlInsideIframe(data.download_url); | ||
setDownloadReachability(data.download_url).then((isBlocked) => { | ||
if (isBlocked) { | ||
// If download is unreachable, try again with default | ||
openUrlInsideIframe(defaultDownloadUrl); | ||
} | ||
}); | ||
// Make sure to append any optional query params to requests | ||
const { apiHost, queryParams } = this.options; | ||
|
||
// If we should download the watermarked representation of the file, generate the representation URL, force | ||
// the correct content disposition, and download | ||
if (shouldDownloadWM(this.file, this.options)) { | ||
const contentUrlTemplate = getProp(this.viewer.getRepresentation(), 'content.url_template'); | ||
if (!contentUrlTemplate) { | ||
this.ui.showNotification(downloadErrorMsg); | ||
return; | ||
} | ||
}); | ||
|
||
const downloadUrl = appendQueryParams( | ||
this.viewer.createContentUrlWithAuthParams(contentUrlTemplate, this.viewer.options.viewer.ASSET), | ||
queryParams | ||
); | ||
|
||
DownloadReachability.downloadWithReachabilityCheck(downloadUrl); | ||
|
||
// Otherwise, get the content download URL of the original file and download | ||
} else { | ||
const getDownloadUrl = appendQueryParams(getDownloadURL(this.file.id, apiHost), queryParams); | ||
get(getDownloadUrl, this.getRequestHeaders()).then((data) => { | ||
const downloadUrl = appendQueryParams(data.download_url, queryParams); | ||
DownloadReachability.downloadWithReachabilityCheck(downloadUrl); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -857,6 +861,22 @@ class Preview extends EventEmitter { | |
// (access stats will not be incremented), but content access is still logged server-side for audit purposes | ||
this.options.disableEventLog = !!options.disableEventLog; | ||
|
||
// Sets how previews of watermarked files behave. | ||
// 'all' - Forces watermarked previews of supported file types regardless of collaboration or permission level, | ||
// except for `Uploader`, which cannot preview. | ||
// 'any' - The default watermarking behavior in the Box Web Application. If the file type supports | ||
// watermarking, all users except for those collaborated as an `Uploader` will see a watermarked | ||
// preview. If the file type cannot be watermarked, users will see a non-watermarked preview if they | ||
// are at least a `Viewer-Uploader` and no preview otherwise. | ||
// 'none' - Forces non-watermarked previews. If the file type cannot be watermarked or the user is not at least | ||
// a `Viewer-Uploader`, no preview is shown. | ||
this.options.previewWMPref = options.previewWMPref || 'any'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the "Pref" at the end adds much There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wanted to distinguish some way between previewWM and downloadWM since one takes a string and one takes a boolean, but I'm open to dropping There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah makes sense, make the download one a boolean like verb by adding should or force to the front? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to prefixing the property name to help us distinguish boolean type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered |
||
|
||
// Whether the download of a watermarked file should be watermarked. This option does not affect non-watermarked | ||
// files. If true, users will be able to download watermarked versions of supported file types as long as they | ||
// have preview permissions (any collaboration role except for `Uploader`). | ||
this.options.downloadWM = !!options.downloadWM; | ||
|
||
// Options that are applicable to certain file ids | ||
this.options.fileOptions = options.fileOptions || {}; | ||
|
||
|
@@ -915,13 +935,20 @@ class Preview extends EventEmitter { | |
* @return {void} | ||
*/ | ||
loadFromServer() { | ||
const { apiHost, queryParams } = this.options; | ||
const { apiHost, previewWMPref, queryParams } = this.options; | ||
const params = Object.assign( | ||
{ | ||
watermark_preference: convertWatermarkPref(previewWMPref) | ||
}, | ||
queryParams | ||
); | ||
|
||
const fileVersionId = this.getFileOption(this.file.id, FILE_OPTION_FILE_VERSION_ID) || ''; | ||
|
||
const tag = Timer.createTag(this.file.id, LOAD_METRIC.fileInfoTime); | ||
Timer.start(tag); | ||
|
||
const fileInfoUrl = appendQueryParams(getURL(this.file.id, fileVersionId, apiHost), queryParams); | ||
const fileInfoUrl = appendQueryParams(getURL(this.file.id, fileVersionId, apiHost), params); | ||
get(fileInfoUrl, this.getRequestHeaders()) | ||
.then(this.handleFileInfoResponse) | ||
.catch(this.handleFetchError); | ||
|
@@ -1024,7 +1051,7 @@ class Preview extends EventEmitter { | |
throw new PreviewError(ERROR_CODE.PERMISSIONS_PREVIEW, __('error_permissions')); | ||
} | ||
|
||
// Show download button if download permissions exist, options allow, and browser has ability | ||
// Show loading download button if user can download | ||
if (canDownload(this.file, this.options)) { | ||
this.ui.showLoadingDownloadButton(this.download); | ||
} | ||
|
@@ -1173,7 +1200,7 @@ class Preview extends EventEmitter { | |
// Log now that loading is finished | ||
this.emitLoadMetrics(); | ||
|
||
// Show or hide print/download buttons | ||
// Show download and print buttons if user can download | ||
if (canDownload(this.file, this.options)) { | ||
this.ui.showDownloadButton(this.download); | ||
|
||
|
@@ -1515,7 +1542,13 @@ class Preview extends EventEmitter { | |
* @return {void} | ||
*/ | ||
prefetchNextFiles() { | ||
const { apiHost, queryParams, skipServerUpdate } = this.options; | ||
const { apiHost, previewWMPref, queryParams, skipServerUpdate } = this.options; | ||
const params = Object.assign( | ||
{ | ||
watermark_preference: convertWatermarkPref(previewWMPref) | ||
}, | ||
queryParams | ||
); | ||
|
||
// Don't bother prefetching when there aren't more files or we need to skip server update | ||
if (this.collection.length < 2 || skipServerUpdate) { | ||
|
@@ -1544,7 +1577,7 @@ class Preview extends EventEmitter { | |
|
||
// Append optional query params | ||
const fileVersionId = this.getFileOption(fileId, FILE_OPTION_FILE_VERSION_ID) || ''; | ||
const fileInfoUrl = appendQueryParams(getURL(fileId, fileVersionId, apiHost), queryParams); | ||
const fileInfoUrl = appendQueryParams(getURL(fileId, fileVersionId, apiHost), params); | ||
|
||
// Prefetch and cache file information and content | ||
get(fileInfoUrl, this.getRequestHeaders(token)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we show a button if they can't download? If we don't know till reps come back this will get interesting...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public method so one could call
preview.download()
directlyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good!