Skip to content

Commit

Permalink
New: Watermarking preferences (#721)
Browse files Browse the repository at this point in the history
Add support for two new watermarking preferences: `previewWMPref` and `downloadWM`. `previewWMPref` allows configuration of the preview experience for watermarked files (do you see watermarked versions of the file or not) and `downloadWM` controls whether watermarked files or the original is downloaded via the download button and download() API.

Also fixed some flaky tests.
  • Loading branch information
tonyjin authored Mar 20, 2018
1 parent 61fa208 commit a49f234
Show file tree
Hide file tree
Showing 15 changed files with 672 additions and 416 deletions.
2 changes: 2 additions & 0 deletions src/i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ notification_annotation_point_mode=Click anywhere to add a comment to the docume
notification_annotation_draw_mode=Press down and drag the pointer to draw on the document
# Notification message shown when the user has a degraded preview experience due to blocked download hosts
notification_degraded_preview=It looks like your connection to {1} is being blocked. We think we can make file previews faster for you. To do that, please ask your network admin to configure firewall settings so that {1} is reachable.
# Notification message shown when a file cannot be downloaded
notification_cannot_download=Sorry! You can't download this file.

# Link Text
link_contact_us=Contact Us
Expand Down
150 changes: 150 additions & 0 deletions src/lib/DownloadReachability.js
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;
103 changes: 68 additions & 35 deletions src/lib/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,7 +39,8 @@ import {
isWatermarked,
getCachedFile,
normalizeFileVersion,
canDownload
canDownload,
shouldDownloadWM
} from './file';
import {
API_HOST,
Expand Down Expand Up @@ -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);
});
}
}

/**
Expand Down Expand Up @@ -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';

// 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 || {};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/lib/RepStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class RepStatus extends EventEmitter {
* @param {string} options.token - Access token
* @param {string} options.sharedLink - Shared link
* @param {string} options.sharedLinkPassword - Shared link password
* @param {string} options.fileId - File ID
* @param {Object} [options.logger] - Optional logger instance
* @return {RepStatus} RepStatus instance
*/
Expand Down
Loading

0 comments on commit a49f234

Please sign in to comment.