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 = '