From a49f2348c850f26c417b0f32e36382c420504a15 Mon Sep 17 00:00:00 2001 From: Tony Jin Date: Tue, 20 Mar 2018 13:03:44 -0700 Subject: [PATCH] New: Watermarking preferences (#721) 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. --- src/i18n/en-US.properties | 2 + src/lib/DownloadReachability.js | 150 ++++++++++++++ src/lib/Preview.js | 103 +++++---- src/lib/RepStatus.js | 1 + .../__tests__/DownloadReachability-test.js | 196 ++++++++++++++++++ src/lib/__tests__/Preview-test.js | 101 +++++---- src/lib/__tests__/RepStatus-test.js | 4 +- .../__tests__/downloadReachability-test.js | 130 ------------ src/lib/__tests__/file-test.js | 58 ++++-- src/lib/__tests__/util-test.js | 68 +++--- src/lib/downloadReachability.js | 120 ----------- src/lib/file.js | 40 +++- src/lib/util.js | 37 +++- src/lib/viewers/BaseViewer.js | 37 ++-- src/lib/viewers/__tests__/BaseViewer-test.js | 41 ++-- 15 files changed, 672 insertions(+), 416 deletions(-) create mode 100644 src/lib/DownloadReachability.js create mode 100644 src/lib/__tests__/DownloadReachability-test.js delete mode 100644 src/lib/__tests__/downloadReachability-test.js delete mode 100644 src/lib/downloadReachability.js diff --git a/src/i18n/en-US.properties b/src/i18n/en-US.properties index 03714ecd7..7890dc4c0 100644 --- a/src/i18n/en-US.properties +++ b/src/i18n/en-US.properties @@ -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 diff --git a/src/lib/DownloadReachability.js b/src/lib/DownloadReachability.js new file mode 100644 index 000000000..af599028f --- /dev/null +++ b/src/lib/DownloadReachability.js @@ -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; diff --git a/src/lib/Preview.js b/src/lib/Preview.js index 60ecd8e32..88e25ccf9 100644 --- a/src/lib/Preview.js +++ b/src/lib/Preview.js @@ -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'; + + // 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)) diff --git a/src/lib/RepStatus.js b/src/lib/RepStatus.js index 51ba53713..c6a6d2e9c 100644 --- a/src/lib/RepStatus.js +++ b/src/lib/RepStatus.js @@ -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 */ diff --git a/src/lib/__tests__/DownloadReachability-test.js b/src/lib/__tests__/DownloadReachability-test.js new file mode 100644 index 000000000..282762338 --- /dev/null +++ b/src/lib/__tests__/DownloadReachability-test.js @@ -0,0 +1,196 @@ +/* eslint-disable no-unused-expressions */ +import 'whatwg-fetch'; +import fetchMock from 'fetch-mock'; +import DownloadReachability from '../DownloadReachability'; +import * as util from '../util'; + +const sandbox = sinon.sandbox.create(); + +const DEFAULT_DOWNLOAD_HOST_PREFIX = 'https://dl.'; +const DOWNLOAD_NOTIFICATION_SHOWN_KEY = 'download_host_notification_shown'; +const DOWNLOAD_HOST_FALLBACK_KEY = 'download_host_fallback'; + +describe('lib/DownloadReachability', () => { + beforeEach(() => { + sessionStorage.clear(); + localStorage.clear(); + }); + + afterEach(() => { + sessionStorage.clear(); + localStorage.clear(); + sandbox.verifyAndRestore(); + + }); + + describe('isCustomDownloadHost()', () => { + it('should be true if the url does not start with the default host prefix but is a dl host', () => { + let url = 'https://dl3.boxcloud.com/foo'; + let result = DownloadReachability.isCustomDownloadHost(url) + expect(result).to.be.true; + + url = 'https://dl.boxcloud.com/foo'; + expect(DownloadReachability.isCustomDownloadHost(url)).to.be.false; + + url = 'https://www.google.com'; + expect(DownloadReachability.isCustomDownloadHost(url)).to.be.false; + + + url = 'https://kld3lk.boxcloud.com'; + expect(DownloadReachability.isCustomDownloadHost(url)).to.be.true; + + url = 'https://dl3.user.inside-box.net'; + expect(DownloadReachability.isCustomDownloadHost(url)).to.be.true; + + + url = 'https://dl.user.inside-box.net'; + expect(DownloadReachability.isCustomDownloadHost(url)).to.be.false; + }); + }); + + describe('replaceDownloadHostWithDefault()', () => { + it('should add the given host to the array of shown hosts', () => { + const blockedHost = 'https://dl3.boxcloud.com'; + + const result = DownloadReachability.setDownloadHostNotificationShown(blockedHost); + + const shownHostsArr = JSON.parse(localStorage.getItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY)) || []; + expect(shownHostsArr).to.contain('https://dl3.boxcloud.com'); + }); + }); + + describe('setDownloadHostFallback()', () => { + it('should set the download host fallback key to be true', () => { + expect(sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY)).to.not.equal('true') + + DownloadReachability.setDownloadHostFallback(); + + expect(sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY)).to.equal('true') + + }); + }); + + describe('isDownloadHostBlocked()', () => { + it('should set the download host fallback key to be true', () => { + expect(DownloadReachability.isDownloadHostBlocked()).to.be.false; + + DownloadReachability.setDownloadHostFallback(); + + expect(DownloadReachability.isDownloadHostBlocked()).to.be.true; + }); + }); + + describe('setDownloadHostNotificationShown()', () => { + it('should set the download host fallback key to be true', () => { + expect(DownloadReachability.isDownloadHostBlocked()).to.be.false; + + DownloadReachability.setDownloadHostFallback(); + + expect(DownloadReachability.isDownloadHostBlocked()).to.be.true; + }); + }); + + describe('getDownloadNotificationToShow()', () => { + beforeEach(() => { + sessionStorage.setItem('download_host_fallback', 'false'); + }); + + it('should return true if we do not have an entry for the given host and our session indicates we are falling back to the default host', () => { + let result = DownloadReachability.getDownloadNotificationToShow('https://foo.com'); + expect(result).to.be.undefined;; + + sessionStorage.setItem('download_host_fallback', 'true'); + result = DownloadReachability.getDownloadNotificationToShow('https://dl5.boxcloud.com'); + expect(result).to.equal('dl5.boxcloud.com'); + + const shownHostsArr = ['dl5.boxcloud.com']; + localStorage.setItem('download_host_notification_shown', JSON.stringify(shownHostsArr)); + result = DownloadReachability.getDownloadNotificationToShow('https://dl5.boxcloud.com'); + expect(result).to.be.undefined; + + }); + }); + + describe('setDownloadReachability()', () => { + afterEach(() => { + fetchMock.restore(); + }) + it('should catch an errored response', () => { + const setDownloadHostFallbackStub = sandbox.stub(DownloadReachability, 'setDownloadHostFallback'); + fetchMock.head('https://dl3.boxcloud.com', {throws: new Error()}) + + return DownloadReachability.setDownloadReachability('https://dl3.boxcloud.com').catch(() => { + expect(setDownloadHostFallbackStub).to.be.called; + }); + }); + }); + + describe('downloadWithReachabilityCheck()', () => { + it('should download with default host if download host is blocked', () => { + sandbox.stub(DownloadReachability, 'isDownloadHostBlocked').returns(true); + sandbox.stub(util, 'openUrlInsideIframe'); + + const downloadUrl = 'https://custom.boxcloud.com/blah'; + const expected = 'https://dl.boxcloud.com/blah'; + + DownloadReachability.downloadWithReachabilityCheck(downloadUrl); + + expect(util.openUrlInsideIframe).to.be.calledWith(expected); + }); + + it('should download with default host if download host is already default', () => { + sandbox.stub(DownloadReachability, 'isDownloadHostBlocked').returns(false); + sandbox.stub(DownloadReachability, 'isCustomDownloadHost').returns(false); + sandbox.stub(util, 'openUrlInsideIframe'); + + const downloadUrl = 'https://dl.boxcloud.com/blah'; + + DownloadReachability.downloadWithReachabilityCheck(downloadUrl); + + expect(util.openUrlInsideIframe).to.be.calledWith(downloadUrl); + }); + + it('should download with the custom download host if host is not blocked', () => { + sandbox.stub(DownloadReachability, 'isDownloadHostBlocked').returns(false); + sandbox.stub(DownloadReachability, 'isCustomDownloadHost').returns(true); + sandbox.stub(util, 'openUrlInsideIframe'); + + const downloadUrl = 'https://custom.boxcloud.com/blah'; + + DownloadReachability.downloadWithReachabilityCheck(downloadUrl); + + expect(util.openUrlInsideIframe).to.be.calledWith(downloadUrl); + }); + + it('should check download reachability for custom host', () => { + sandbox.stub(DownloadReachability, 'isDownloadHostBlocked').returns(false); + sandbox.stub(DownloadReachability, 'isCustomDownloadHost').returns(true); + sandbox.stub(DownloadReachability, 'setDownloadReachability').returns(Promise.resolve(false)); + sandbox.stub(util, 'openUrlInsideIframe'); + + const downloadUrl = 'https://custom.boxcloud.com/blah'; + + DownloadReachability.downloadWithReachabilityCheck(downloadUrl); + + expect(DownloadReachability.setDownloadReachability).to.be.calledWith(downloadUrl); + }); + + it('should retry download with default host if custom host is blocked', (done) => { + sandbox.stub(DownloadReachability, 'isDownloadHostBlocked').returns(false); + sandbox.stub(DownloadReachability, 'isCustomDownloadHost').returns(true); + sandbox.stub(DownloadReachability, 'setDownloadReachability').returns(new Promise((resolve) => { + resolve(true); + done(); + })); + sandbox.stub(util, 'openUrlInsideIframe'); + + const downloadUrl = 'https://custom.boxcloud.com/blah'; + const defaultDownloadUrl = 'https://dl.boxcloud.com/blah'; + + DownloadReachability.downloadWithReachabilityCheck(downloadUrl); + + expect(util.openUrlInsideIframe.getCall(0).args[0]).to.equal(downloadUrl); + expect(util.openUrlInsideIframe.getCall(0).args[1]).to.equal(defaultDownloadUrl); + }); + }); +}); diff --git a/src/lib/__tests__/Preview-test.js b/src/lib/__tests__/Preview-test.js index fdb59fe36..c74a2c2a4 100644 --- a/src/lib/__tests__/Preview-test.js +++ b/src/lib/__tests__/Preview-test.js @@ -6,9 +6,9 @@ import loaders from '../loaders'; import Logger from '../Logger'; import Browser from '../Browser'; import PreviewError from '../PreviewError'; +import DownloadReachability from '../DownloadReachability'; import * as file from '../file'; import * as util from '../util'; -import * as dr from '../downloadReachability'; import { API_HOST, CLASS_NAVIGATION_VISIBILITY, PERMISSION_PREVIEW } from '../constants'; import { VIEWER_EVENT, ERROR_CODE, LOAD_METRIC, PREVIEW_METRIC } from '../events'; import Timer from '../Timer'; @@ -743,54 +743,85 @@ describe('lib/Preview', () => { } }); - stubs.reachabilityPromise = Promise.resolve(true); - stubs.canDownload = sandbox.stub(file, 'canDownload'); - stubs.get = sandbox.stub(util, 'get').returns(stubs.promise); - stubs.get = sandbox.stub(dr, 'setDownloadReachability').returns(stubs.reachabilityPromise); - stubs.openUrlInsideIframe = sandbox.stub(util, 'openUrlInsideIframe'); - stubs.getRequestHeaders = sandbox.stub(preview, 'getRequestHeaders'); - stubs.getDownloadURL = sandbox.stub(file, 'getDownloadURL'); - stubs.isDownloadHostBlocked = sandbox.stub(dr, 'isDownloadHostBlocked'); - stubs.isCustomDownloadHost = sandbox.stub(dr, 'isCustomDownloadHost'); - stubs.replaceDownloadHostWithDefault = sandbox.stub(dr, 'replaceDownloadHostWithDefault').returns('default'); + preview.ui = { + showNotification: sandbox.stub() + }; + preview.viewer = { + getRepresentation: sandbox.stub(), + createContentUrlWithAuthParams: sandbox.stub(), + options: { + viewer: { + ASSET: '' + } + } + }; + sandbox.stub(file, 'canDownload'); + sandbox.stub(file, 'shouldDownloadWM'); + sandbox.stub(util, 'openUrlInsideIframe'); + sandbox.stub(util, 'appendQueryParams'); + sandbox.stub(DownloadReachability, 'downloadWithReachabilityCheck'); + + sandbox.stub(file, 'getDownloadURL'); + sandbox.stub(preview, 'getRequestHeaders'); + sandbox.stub(util, 'get'); }); - it('should not do anything if file cannot be downloaded', () => { - stubs.canDownload.returns(false); + it('should show error notification and not download file if file cannot be downloaded', () => { + file.canDownload.returns(false); preview.download(); - expect(stubs.openUrlInsideIframe).to.not.be.called; + expect(preview.ui.showNotification).to.be.called; + expect(util.openUrlInsideIframe).to.not.be.called; }); - it('open the default download URL in an iframe if the custom host is blocked or if we were given the default', () => { - stubs.canDownload.returns(true); - stubs.isDownloadHostBlocked.returns(true); - stubs.isCustomDownloadHost.returns(true); + it('should show error notification and not download watermarked file if file should be downloaded as watermarked, but file does not have a previewable representation', () => { + file.canDownload.returns(true); + file.shouldDownloadWM.returns(true); + preview.viewer.getRepresentation.returns({}); preview.download(); - return stubs.promise.then((data) => { - expect(stubs.openUrlInsideIframe).to.be.calledWith('default'); - }); - stubs.isDownloadHostBlocked.returns(false); - stubs.isCustomDownloadHost.returns(false); + expect(preview.ui.showNotification).to.be.called; + expect(util.openUrlInsideIframe).to.not.be.called; + }); + + it('should download watermarked representation if file should be downloaded as watermarked', () => { + file.canDownload.returns(true); + file.shouldDownloadWM.returns(true); + + const template = 'someTemplate'; + const representation = { + content: { + url_template: template + } + }; + const url = 'someurl'; + + preview.viewer.getRepresentation.returns(representation); + preview.viewer.createContentUrlWithAuthParams.withArgs(template, '').returns(url); + + util.appendQueryParams.withArgs(url).returns(url); preview.download(); - return stubs.promise.then((data) => { - expect(stubs.openUrlInsideIframe).to.be.calledWith('default'); - }); + + expect(DownloadReachability.downloadWithReachabilityCheck).to.be.calledWith(url); }); + it('should download original file if file should not be downloaded as watermarked', () => { + file.canDownload.returns(true); + file.shouldDownloadWM.returns(false); - it('should check download reachability and fallback if we do not know the status of our custom host', () => { - stubs.canDownload.returns(true); - stubs.isCustomDownloadHost.returns(true); + const url = 'someurl'; + util.appendQueryParams.withArgs(url).returns(url); + + const promise = Promise.resolve({ + download_url: url + }); + util.get.returns(promise); preview.download(); - return stubs.promise.then((data) => { - expect(stubs.openUrlInsideIframe).to.be.calledWith(data.download_url); - return stubs.reachabilityPromise.then(() => { - expect(stubs.openUrlInsideIframe).to.be.calledWith('default'); - }); + + return promise.then((data) => { + expect(DownloadReachability.downloadWithReachabilityCheck).to.be.calledWith(url); }); }); }); @@ -809,7 +840,7 @@ describe('lib/Preview', () => { it('should reload preview by default', () => { preview.file = { id: '1' }; sandbox.stub(preview, 'load'); - preview.updateToken('dr-strange'); + preview.updateToken('DownloadReachability-strange'); expect(preview.reload).to.be.called; }); diff --git a/src/lib/__tests__/RepStatus-test.js b/src/lib/__tests__/RepStatus-test.js index 476d4dbd7..30268af3f 100644 --- a/src/lib/__tests__/RepStatus-test.js +++ b/src/lib/__tests__/RepStatus-test.js @@ -89,7 +89,7 @@ describe('lib/RepStatus', () => { describe('destroy()', () => { it('should clear the status timeout', () => { - sandbox.mock(window).expects('clearTimeout').withArgs(repStatus.statusTimeout); + sandbox.mock(window).expects('clearTimeout'); repStatus.destroy(); }); }); @@ -109,8 +109,6 @@ describe('lib/RepStatus', () => { }) ); - sandbox.mock(window).expects('clearTimeout'); - return repStatus.updateStatus().then(() => { expect(repStatus.representation.status.state).to.equal(state); expect(repStatus.handleResponse).to.be.called; diff --git a/src/lib/__tests__/downloadReachability-test.js b/src/lib/__tests__/downloadReachability-test.js deleted file mode 100644 index ace9a24d4..000000000 --- a/src/lib/__tests__/downloadReachability-test.js +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable no-unused-expressions */ -import 'whatwg-fetch'; -import fetchMock from 'fetch-mock'; -import * as dr from '../downloadReachability'; - -const sandbox = sinon.sandbox.create(); - -const DEFAULT_DOWNLOAD_HOST_PREFIX = 'https://dl.'; -const DOWNLOAD_NOTIFICATION_SHOWN_KEY = 'download_host_notification_shown'; -const DOWNLOAD_HOST_FALLBACK_KEY = 'download_host_fallback'; - -describe('lib/downloadReachability', () => { - beforeEach(() => { - sessionStorage.clear(); - localStorage.clear(); - - }); - - afterEach(() => { - sessionStorage.clear(); - localStorage.clear(); - sandbox.verifyAndRestore(); - - }); - - describe('isCustomDownloadHost()', () => { - it('should be true if the url does not start with the default host prefix but is a dl host', () => { - let url = 'https://dl3.boxcloud.com/foo'; - let result = dr.isCustomDownloadHost(url) - expect(result).to.be.true; - - url = 'https://dl.boxcloud.com/foo'; - expect(dr.isCustomDownloadHost(url)).to.be.false; - - url = 'https://www.google.com'; - expect(dr.isCustomDownloadHost(url)).to.be.false; - - - url = 'https://kld3lk.boxcloud.com'; - expect(dr.isCustomDownloadHost(url)).to.be.true; - - url = 'https://dl3.user.inside-box.net'; - expect(dr.isCustomDownloadHost(url)).to.be.true; - - - url = 'https://dl.user.inside-box.net'; - expect(dr.isCustomDownloadHost(url)).to.be.false; - }); - }); - - describe('replaceDownloadHostWithDefault()', () => { - it('should add the given host to the array of shown hosts', () => { - const blockedHost = 'https://dl3.boxcloud.com'; - - const result = dr.setDownloadHostNotificationShown(blockedHost); - - const shownHostsArr = JSON.parse(localStorage.getItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY)) || []; - expect(shownHostsArr).to.contain('https://dl3.boxcloud.com'); - }); - }); - - describe('setDownloadHostFallback()', () => { - it('should set the download host fallback key to be true', () => { - expect(sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY)).to.not.equal('true') - - dr.setDownloadHostFallback(); - - expect(sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY)).to.equal('true') - - }); - }); - - describe('isDownloadHostBlocked()', () => { - it('should set the download host fallback key to be true', () => { - expect(dr.isDownloadHostBlocked()).to.be.false; - - dr.setDownloadHostFallback(); - - expect(dr.isDownloadHostBlocked()).to.be.true; - }); - }); - - describe('setDownloadHostNotificationShown()', () => { - it('should set the download host fallback key to be true', () => { - expect(dr.isDownloadHostBlocked()).to.be.false; - - dr.setDownloadHostFallback(); - - expect(dr.isDownloadHostBlocked()).to.be.true; - }); - }); - - describe('downloadNotificationToShow()', () => { - beforeEach(() => { - sessionStorage.setItem('download_host_fallback', 'false'); - }); - - it('should return true if we do not have an entry for the given host and our session indicates we are falling back to the default host', () => { - let result = dr.downloadNotificationToShow('https://foo.com'); - expect(result).to.be.undefined;; - - sessionStorage.setItem('download_host_fallback', 'true'); - result = dr.downloadNotificationToShow('https://dl5.boxcloud.com'); - expect(result).to.equal('dl5.boxcloud.com'); - - const shownHostsArr = ['dl5.boxcloud.com']; - localStorage.setItem('download_host_notification_shown', JSON.stringify(shownHostsArr)); - result = dr.downloadNotificationToShow('https://dl5.boxcloud.com'); - expect(result).to.be.undefined; - - }); - }); - - describe('setDownloadReachability()', () => { - afterEach(() => { - fetchMock.restore(); - }) - it('should catch an errored response', () => { - const setDownloadHostFallbackStub = sandbox.stub(dr, 'setDownloadHostFallback'); - fetchMock.head('https://dl3.boxcloud.com', {throws: new Error()}) - - return dr.setDownloadReachability('https://dl3.boxcloud.com').catch(() => { - expect(setDownloadHostFallbackStub).to.be.called; - }) - - - - }); - }); -}); \ No newline at end of file diff --git a/src/lib/__tests__/file-test.js b/src/lib/__tests__/file-test.js index e25789623..5e382e791 100644 --- a/src/lib/__tests__/file-test.js +++ b/src/lib/__tests__/file-test.js @@ -14,7 +14,8 @@ import { normalizeFileVersion, getCachedFile, isVeraProtectedFile, - canDownload + canDownload, + shouldDownloadWM } from '../file'; const sandbox = sinon.sandbox.create(); @@ -353,6 +354,26 @@ describe('lib/file', () => { }); }); + describe('shouldDownloadWM()', () => { + [ + [false, false, false], + [false, true, false], + [true, true, true], + [true, false, false], + ].forEach(([downloadWM, isWatermarked, expected]) => { + it('should return whether we should download the watermarked representation or original file', () => { + const previewOptions = { downloadWM }; + const file = { + watermark_info: { + is_watermarked: isWatermarked + } + }; + + expect(shouldDownloadWM(file, previewOptions)).to.equal(expected); + }); + }); + }); + describe('canDownload()', () => { let file; let options; @@ -361,7 +382,11 @@ describe('lib/file', () => { file = { is_download_available: false, permissions: { - can_download: false + can_download: false, + can_preview: false + }, + watermark_info: { + is_watermarked: false } }; options = { @@ -370,17 +395,26 @@ describe('lib/file', () => { }); [ - [false, false, false, false, false], - [false, false, false, true, false], - [false, false, true, false, false], - [false, true, false, false, false], - [true, false, false, false, false], - [true, true, true, true, true], - ].forEach(([isDownloadable, isDownloadEnabled, havePermission, isBrowserSupported, expectedResult]) => { - it('should only return true if all of: file is downloadable, download is enabled, user has permissions, and browser can download is true', () => { - file.permissions.can_download = havePermission; - file.is_download_available = isDownloadable + // Can download original + [false, false, false, false, false, false, false, false], + [false, false, false, true, false, false, false, false], + [false, false, true, false, false, false, false, false], + [false, true, false, false, false, false, false, false], + [true, false, false, false, false, false, false, false], + [true, true, true, true, false, false, false, true], + + // Can download watermarked (don't need download permission) + [true, true, false, true, true, false, false, false], + [true, true, false, true, true, true, false, false], + [true, true, false, true, true, true, true, true], + ].forEach(([isDownloadable, isDownloadEnabled, hasDownloadPermission, isBrowserSupported, hasPreviewPermission, isWatermarked, downloadWM, expectedResult]) => { + it('should return true if original or watermarked file can be downloaded', () => { + file.permissions.can_download = hasDownloadPermission; + file.permissions.can_preview = hasPreviewPermission; + file.is_download_available = isDownloadable; + file.watermark_info.is_watermarked = isWatermarked; options.showDownload = isDownloadable; + options.downloadWM = downloadWM; sandbox.stub(Browser, 'canDownload').returns(isBrowserSupported); expect(canDownload(file, options)).to.equal(expectedResult); diff --git a/src/lib/__tests__/util-test.js b/src/lib/__tests__/util-test.js index f4d309399..ab9de9630 100644 --- a/src/lib/__tests__/util-test.js +++ b/src/lib/__tests__/util-test.js @@ -99,23 +99,6 @@ describe('lib/util', () => { expect(typeof response === 'object').to.be.true; }); }); - - it('set the download host fallback and try again if we\'re fetching from a non default host', () => { - url = 'dl7.boxcloud.com' - fetchMock.get(url, { - status: 500 - }); - - return util.get(url, 'any') - .then(() => { - expect(response.status).to.equal(200); - }) - .catch(() => { - fetchMock.get(url, { - status: 200 - }); - }) - }); }); describe('post()', () => { @@ -187,20 +170,31 @@ describe('lib/util', () => { }); }); - describe('openUrlInsideIframe()', () => { - it('should return a download iframe with correct source', () => { - const src = 'admiralackbar'; - const iframe = util.openUrlInsideIframe(src); - expect(iframe.getAttribute('id')).to.equal('downloadiframe'); - expect(iframe.getAttribute('src')).to.equal(src); + describe('iframe', () => { + let iframe; + + afterEach(() => { + if (iframe && iframe.parentElement) { + iframe.parentElement.removeChild(iframe); + } + }); + + describe('openUrlInsideIframe()', () => { + it('should return a download iframe with correct source', () => { + const src = 'admiralackbar'; + iframe = util.openUrlInsideIframe(src); + + expect(iframe.getAttribute('id')).to.equal('downloadiframe'); + expect(iframe.getAttribute('src')).to.equal(src); + }); }); - }); - describe('openContentInsideIframe()', () => { - it('should return a download iframe with correct content', () => { - const src = 'moncalamari'; - const iframe = util.openContentInsideIframe(src); - expect(iframe.contentDocument.body.innerHTML).to.equal(src); + describe('openContentInsideIframe()', () => { + it('should return a download iframe with content', () => { + const content = '
moncalamari
'; + iframe = util.openContentInsideIframe(content); + expect(iframe.contentDocument.querySelector('.test')).to.exist; + }); }); }); @@ -836,7 +830,7 @@ describe('lib/util', () => { }); describe('isBoxWebApp()', () => { - [ + [ ['https://test.app.box.com', true], ['https://foo.ent.box.com', true], ['https://bar.app.boxcn.net', true], @@ -849,6 +843,18 @@ describe('lib/util', () => { sandbox.stub(Location, 'getHostname').returns(hostname); expect(util.isBoxWebApp()).to.equal(expectedResult); }); - }) + }); + }); + + describe('convertWatermarkPref()', () => { + [ + ['any', ''], + ['all', 'only_watermarked'], + ['none', 'only_non_watermarked'] + ].forEach(([previewWMPref, expected]) => { + it('should convert previewWMPref to value expected by the API', () => { + expect(util.convertWatermarkPref(previewWMPref)).to.equal(expected); + }); + }); }); }); diff --git a/src/lib/downloadReachability.js b/src/lib/downloadReachability.js deleted file mode 100644 index 98ed3ff33..000000000 --- a/src/lib/downloadReachability.js +++ /dev/null @@ -1,120 +0,0 @@ -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]+./; - -/** - * 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 - */ -export function 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 - */ -export function 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 - */ -export function 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} - */ -export function 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 - */ -export function 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} - */ -export function 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 - */ -export function downloadNotificationToShow(downloadUrl) { - const contentHostname = getHostnameFromUrl(downloadUrl); - const shownHostsArr = JSON.parse(localStorage.getItem(DOWNLOAD_NOTIFICATION_SHOWN_KEY)) || []; - - return sessionStorage.getItem(DOWNLOAD_HOST_FALLBACK_KEY) === 'true' && - !shownHostsArr.includes(contentHostname) && - 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} - */ -export function setDownloadReachability(downloadUrl) { - return fetch(downloadUrl, { method: 'HEAD' }) - .then(() => { - return Promise.resolve(false); - }) - .catch(() => { - setDownloadHostFallback(); - return Promise.resolve(true); - }); -} diff --git a/src/lib/file.js b/src/lib/file.js index 0b5d4b847..66cb99ead 100644 --- a/src/lib/file.js +++ b/src/lib/file.js @@ -1,6 +1,6 @@ import Browser from './Browser'; import { getProp, appendQueryParams } from './util'; -import { ORIGINAL_REP_NAME, PERMISSION_DOWNLOAD } from './constants'; +import { ORIGINAL_REP_NAME, PERMISSION_DOWNLOAD, PERMISSION_PREVIEW } from './constants'; // List of Box Content API fields that the Preview library requires for every file. Updating this list is most likely // a breaking change and should be done with care. Clients that leverage functionality dependent on this format @@ -264,17 +264,43 @@ export function isVeraProtectedFile(file) { } /** - * Helper to determine whether a file can be downloaded based on permissions, file status, browser capability, and - * Preview options. + * Helper to determine whether we should download the watermarked representation of a file or original. * * @param {Object} file - Box file object * @param {Object} previewOptions - Preview options - * @return {boolean} Whether file can be downloaded + * @return {boolean} Whether to download watermarked representation or not + */ +export function shouldDownloadWM(file, previewOptions) { + const { downloadWM } = previewOptions; + return downloadWM && isWatermarked(file); +} + +/** + * Helper to determine whether a the original or watermarked representation of a file can be downloaded based on + * permissions, file status, browser capability, and preview options. + * + * @param {Object} file - Box file object + * @param {Object} previewOptions - Preview options + * @return {boolean} Whether file can be downloaded (original OR watermarked representation) */ export function canDownload(file, previewOptions) { const { is_download_available: isFileDownloadable } = file; - const { showDownload: isDownloadEnabled } = previewOptions; - const havePermission = checkPermission(file, PERMISSION_DOWNLOAD); + const { showDownload } = previewOptions; + const hasDownloadPermission = checkPermission(file, PERMISSION_DOWNLOAD); + const hasPreviewPermission = checkPermission(file, PERMISSION_PREVIEW); + + // All downloads require that the file is downloadable as reported by the API, Preview is configured to show the + // download button, and the browser supports downloads + const passBaseDownloadCheck = isFileDownloadable && showDownload && Browser.canDownload(); + + // Can download the original file if the base check passes and user has explicit download permissions + const canDownloadOriginal = passBaseDownloadCheck && hasDownloadPermission; + + // Can download watermarked representation if base check passes, user has preview permissions, Preview is configured + // to force watermarked downloads, and the file is watermarked + const canDownloadWatermarked = passBaseDownloadCheck && + hasPreviewPermission && + shouldDownloadWM(file, previewOptions); - return havePermission && isFileDownloadable && isDownloadEnabled && Browser.canDownload(); + return canDownloadOriginal || canDownloadWatermarked; } diff --git a/src/lib/util.js b/src/lib/util.js index 3360dc5e4..61b46b48c 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -1,8 +1,7 @@ import Uri from 'jsuri'; import 'whatwg-fetch'; - +import DownloadReachability from './DownloadReachability'; import Location from './Location'; -import { isDownloadHostBlocked, replaceDownloadHostWithDefault } from './downloadReachability'; const HEADER_CLIENT_NAME = 'X-Box-Client-Name'; const HEADER_CLIENT_VERSION = 'X-Box-Client-Version'; @@ -102,15 +101,17 @@ function xhr(method, url, headers = {}, data = {}) { */ function createDownloadIframe() { let iframe = document.querySelector('#downloadiframe'); + + // If no download iframe exists, create a new one if (!iframe) { - // if no existing iframe create a new one iframe = document.createElement('iframe'); iframe.setAttribute('id', 'downloadiframe'); iframe.style.display = 'none'; iframe = document.body.appendChild(iframe); } + // Clean the iframe up - iframe.contentDocument.write(''); + iframe.src = 'about:blank'; return iframe; } @@ -227,8 +228,10 @@ export function openUrlInsideIframe(url) { */ export function openContentInsideIframe(content) { const iframe = createDownloadIframe(); - iframe.contentDocument.body.innerHTML = content; - iframe.contentDocument.close(); + if (iframe.contentDocument) { + iframe.contentDocument.write(content); + iframe.contentDocument.close(); + } return iframe; } @@ -406,9 +409,9 @@ export function appendAuthParams(url, token = '', sharedLink = '', password = '' * @return {string} Content url */ export function createContentUrl(template, asset) { - if (isDownloadHostBlocked()) { + if (DownloadReachability.isDownloadHostBlocked()) { // eslint-disable-next-line - template = replaceDownloadHostWithDefault(template); + template = DownloadReachability.replaceDownloadHostWithDefault(template); } return template.replace('{+asset_path}', asset || ''); } @@ -905,3 +908,21 @@ export function isBoxWebApp() { const boxHostnameRegex = /(app|ent)\.(box\.com|boxcn\.net|boxenterprise\.net)/; return boxHostnameRegex.test(Location.getHostname()); } + +/** + * Converts the developer-friendly Preview watermarking preference values to the values expected by the API. + * + * @param {string} previewWMPref - Preview watermarking preference as passed into Preview + * @return {string} Box API watermarking preference value + */ +export function convertWatermarkPref(previewWMPref) { + let value = ''; + + if (previewWMPref === 'all') { + value = 'only_watermarked'; + } else if (previewWMPref === 'none') { + value = 'only_non_watermarked'; + } + + return value; +} diff --git a/src/lib/viewers/BaseViewer.js b/src/lib/viewers/BaseViewer.js index 1ab686d12..e26e62a47 100644 --- a/src/lib/viewers/BaseViewer.js +++ b/src/lib/viewers/BaseViewer.js @@ -3,6 +3,8 @@ import cloneDeep from 'lodash/cloneDeep'; import debounce from 'lodash/debounce'; import fullscreen from '../Fullscreen'; import RepStatus from '../RepStatus'; +import Browser from '../Browser'; +import DownloadReachability from '../DownloadReachability'; import { getProp, appendQueryParams, @@ -15,17 +17,6 @@ import { createAssetUrlCreator, replacePlaceholders } from '../util'; - -import { - setDownloadReachability, - isCustomDownloadHost, - replaceDownloadHostWithDefault, - setDownloadHostNotificationShown, - downloadNotificationToShow, - getHostnameFromUrl -} from '../downloadReachability'; - -import Browser from '../Browser'; import { CLASS_FULLSCREEN, CLASS_FULLSCREEN_UNSUPPORTED, @@ -320,10 +311,13 @@ class BaseViewer extends EventEmitter { return; } - if (isCustomDownloadHost(downloadURL)) { - setDownloadReachability(downloadURL).then((isBlocked) => { + if (DownloadReachability.isCustomDownloadHost(downloadURL)) { + DownloadReachability.setDownloadReachability(downloadURL).then((isBlocked) => { if (isBlocked) { - this.emitMetric(DOWNLOAD_REACHABILITY_METRICS.DOWNLOAD_BLOCKED, getHostnameFromUrl(downloadURL)); + this.emitMetric( + DOWNLOAD_REACHABILITY_METRICS.DOWNLOAD_BLOCKED, + DownloadReachability.getHostnameFromUrl(downloadURL) + ); } }); } @@ -395,7 +389,7 @@ class BaseViewer extends EventEmitter { createContentUrl(template, asset) { if (this.hasRetriedContentDownload) { // eslint-disable-next-line - template = replaceDownloadHostWithDefault(template); + template = DownloadReachability.replaceDownloadHostWithDefault(template); } // Append optional query params @@ -464,7 +458,7 @@ class BaseViewer extends EventEmitter { */ viewerLoadHandler(event) { const contentTemplate = getProp(this.options, 'representation.content.url_template', ''); - const downloadHostToNotify = downloadNotificationToShow(contentTemplate); + const downloadHostToNotify = DownloadReachability.getDownloadNotificationToShow(contentTemplate); if (downloadHostToNotify) { this.previewUI.notification.show( replacePlaceholders(__('notification_degraded_preview'), [downloadHostToNotify]), @@ -472,7 +466,7 @@ class BaseViewer extends EventEmitter { true ); - setDownloadHostNotificationShown(downloadHostToNotify); + DownloadReachability.setDownloadHostNotificationShown(downloadHostToNotify); this.emitMetric(DOWNLOAD_REACHABILITY_METRICS.NOTIFICATION_SHOWN, { host: downloadHostToNotify }); @@ -1030,6 +1024,15 @@ class BaseViewer extends EventEmitter { }) ); } + + /** + * Returns the representation used for Preview. + * + * @return {Object} Box representation used/to be used by Preview + */ + getRepresentation() { + return this.options.representation; + } } export default BaseViewer; diff --git a/src/lib/viewers/__tests__/BaseViewer-test.js b/src/lib/viewers/__tests__/BaseViewer-test.js index ef462c156..648bb53a8 100644 --- a/src/lib/viewers/__tests__/BaseViewer-test.js +++ b/src/lib/viewers/__tests__/BaseViewer-test.js @@ -4,10 +4,9 @@ import BaseViewer from '../BaseViewer'; import Browser from '../../Browser'; import RepStatus from '../../RepStatus'; import PreviewError from '../../PreviewError'; +import DownloadReachability from '../../DownloadReachability'; import fullscreen from '../../Fullscreen'; import * as util from '../../util'; -import * as dr from '../../downloadReachability'; - import * as file from '../../file'; import * as icons from '../../icons/icons'; import * as constants from '../../constants'; @@ -221,8 +220,8 @@ describe('lib/viewers/BaseViewer', () => { describe('handleDownloadError()', () => { beforeEach(() => { sandbox.stub(base, 'triggerError'); - sandbox.stub(dr, 'isCustomDownloadHost'); - sandbox.stub(dr, 'setDownloadReachability'); + sandbox.stub(DownloadReachability, 'isCustomDownloadHost'); + sandbox.stub(DownloadReachability, 'setDownloadReachability'); sandbox.stub(base, 'load'); sandbox.stub(base, 'emitMetric'); }); @@ -236,18 +235,18 @@ describe('lib/viewers/BaseViewer', () => { it('should retry load, and check download reachability if we are on a custom host', () => { base.hasRetriedContentDownload = false; - dr.isCustomDownloadHost.returns(false); + DownloadReachability.isCustomDownloadHost.returns(false); base.handleDownloadError('error', 'https://dl.boxcloud.com'); expect(base.load).to.be.called; - expect(dr.setDownloadReachability).to.be.not.called; + expect(DownloadReachability.setDownloadReachability).to.be.not.called; base.hasRetriedContentDownload = false; // Now try on a custom host - dr.isCustomDownloadHost.returns(true); - dr.setDownloadReachability.returns(Promise.resolve(true)) + DownloadReachability.isCustomDownloadHost.returns(true); + DownloadReachability.setDownloadReachability.returns(Promise.resolve(true)) base.handleDownloadError('error', 'https://dl3.boxcloud.com'); - expect(dr.setDownloadReachability).to.be.called; + expect(DownloadReachability.setDownloadReachability).to.be.called; }); }); @@ -366,12 +365,11 @@ describe('lib/viewers/BaseViewer', () => { it('should fallback to the default host if we have retried', () => { base.hasRetriedContentDownload = true; - sandbox.stub(dr, 'replaceDownloadHostWithDefault'); + sandbox.stub(DownloadReachability, 'replaceDownloadHostWithDefault'); sandbox.stub(util, 'createContentUrl'); - base.createContentUrl('https://dl3.boxcloud.com', ''); - expect(dr.replaceDownloadHostWithDefault).to.be.called; + expect(DownloadReachability.replaceDownloadHostWithDefault).to.be.called; }); }); @@ -457,12 +455,12 @@ describe('lib/viewers/BaseViewer', () => { url_template: 'dl.boxcloud.com' } }; - stubs.downloadNotificationToShow = sandbox.stub(dr, 'downloadNotificationToShow').returns(undefined); + stubs.getDownloadNotificationToShow = sandbox.stub(DownloadReachability, 'getDownloadNotificationToShow').returns(undefined); }); it('should show the notification if downloads are degraded and we have not shown the notification yet', () => { - const result = stubs.downloadNotificationToShow.returns('dl3.boxcloud.com'); + const result = stubs.getDownloadNotificationToShow.returns('dl3.boxcloud.com'); base.previewUI = { notification: { @@ -471,11 +469,11 @@ describe('lib/viewers/BaseViewer', () => { } } - sandbox.stub(dr, 'setDownloadHostNotificationShown'); - + sandbox.stub(DownloadReachability, 'setDownloadHostNotificationShown'); + base.viewerLoadHandler({ scale: 1.5 }); expect(base.previewUI.notification.show).to.be.called; - expect(dr.setDownloadHostNotificationShown).to.be.called; + expect(DownloadReachability.setDownloadHostNotificationShown).to.be.called; }); it('should set the scale if it exists', () => { @@ -1210,7 +1208,7 @@ describe('lib/viewers/BaseViewer', () => { expect(base.emit).to.be.calledWith('annotatorevent', data); }); - it('should disable controls and enter draw anontation mode with notification', () => { + it('should disable controls and enter drawing anontation mode with notification', () => { const data = { event: ANNOTATOR_EVENT.modeEnter, data: { @@ -1316,4 +1314,11 @@ describe('lib/viewers/BaseViewer', () => { expect(combinedOptions.localizedStrings).to.not.be.undefined; }); }); + + describe('getRepresentation()', () => { + it('should return the representation the viewer is/will use to preview', () => { + base.options.representation = { some: 'stuff' }; + expect(base.getRepresentation()).to.equal(base.options.representation); + }); + }); });