Skip to content
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

Merged
merged 2 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

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...

Copy link
Contributor Author

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() directly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good!

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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the "Pref" at the end adds much

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Pref

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to prefixing the property name to help us distinguish boolean type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered shouldDownloadWM and forceDownloadWM but I'm going to keep as downloadWM since download is already a verb and I interpret the option as downloadWatermarked, which implies true/false to me, whereas I interpret previewWMPref as previewWatermark(ed/ing)Preference, which implies options other than just true/false.


// 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