Skip to content

Commit

Permalink
New: platform excel online fork (#101)
Browse files Browse the repository at this point in the history
* New: platform excel online fork

* Update: Updating tests
  • Loading branch information
Jeremy Press authored May 16, 2017
1 parent 0424e16 commit a247b9d
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 35 deletions.
138 changes: 128 additions & 10 deletions src/lib/viewers/office/OfficeViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const LOAD_TIMEOUT_MS = 120000;
const SAFARI_PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print
const PRINT_DIALOG_TIMEOUT_MS = 500;

// @TODO(jpress): replace with discovery and XML parsing once Microsoft allows CORS
const EXCEL_ONLINE_EMBED_URL = 'https://excel.officeapps.live.com/x/_layouts/xlembed.aspx';
const OFFICE_ONLINE_IFRAME_NAME = 'office-online-iframe';
const MESSAGE_HOST_READY = 'Host_PostmessageReady';

@autobind
class OfficeViewer extends BaseViewer {

Expand All @@ -24,6 +29,9 @@ class OfficeViewer extends BaseViewer {
setup() {
// Call super() first to set up common layout
super.setup();
// Set to false only in the WebApp, everywhere else we want to avoid hitting a runmode.
// This flag will be removed once we run the entire integration through the client.
this.platformSetup = this.options.viewers.Office ? !!this.options.viewers.Office.shouldUsePlatformSetup : true;
this.setupIframe();
this.initPrint();
this.setupPDFUrl();
Expand Down Expand Up @@ -147,31 +155,141 @@ class OfficeViewer extends BaseViewer {
* @return {void}
*/
setupIframe() {
this.iframeEl = this.containerEl.appendChild(document.createElement('iframe'));
this.iframeEl.setAttribute('width', '100%');
this.iframeEl.setAttribute('height', '100%');
this.iframeEl.setAttribute('frameborder', 0);
this.iframeEl.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');
const { appHost, apiHost, file, sharedLink, location: { locale } } = this.options;
const iframeEl = this.createIframeElement();
this.containerEl.appendChild(iframeEl);

if (this.platformSetup) {
const formEl = this.createFormElement(apiHost, file.id, sharedLink, locale);
// Submitting the form securely passes a Preview access token to
// Microsoft so they can hit our WOPI endpoints.
formEl.submit();

// Tell Office Online that we are ready to receive messages
iframeEl.contentWindow.postMessage(MESSAGE_HOST_READY, window.location.origin);
} else {
iframeEl.src = this.setupRunmodeURL(appHost, file.id, sharedLink);
}
}

const { appHost, file, sharedLink } = this.options;
/**
* Sets up the runmode URL that fills a wrapper iframe around the Office Online iframe.
*
* @private
* @param {string} appHost - Application host
* @param {string} fileId - File ID
* @param {string} sharedLink - Shared link which may be a vanity URL
* @return {string} Runmode URL
*/
setupRunmodeURL(appHost, fileId, sharedLink) {
// @TODO(jpress): Combine with setupWopiSrc Logic when removing the platform fork
let src = `${appHost}/integrations/officeonline/openExcelOnlinePreviewer`;

if (sharedLink) {
// Find the shared or vanity name
const sharedName = sharedLink.split('/s/')[1];
if (sharedName) {
src += `?s=${sharedName}&fileId=${file.id}`;
src = `${src}?s=${sharedName}&fileId=${fileId}`;
} else {
const tempAnchor = document.createElement('a');
tempAnchor.href = sharedLink;
const vanitySubdomain = tempAnchor.hostname.split('.')[0];
const vanityName = tempAnchor.pathname.split('/v/')[1];
src += `?v=${vanityName}&vanity_subdomain=${vanitySubdomain}&fileId=${file.id}`;
src = `${src}?v=${vanityName}&vanity_subdomain=${vanitySubdomain}&fileId=${fileId}`;
}
} else {
src += `?fileId=${file.id}`;
src = `${src}?fileId=${fileId}`;
}

return src;
}

/**
* Sets up the WOPI src that is used in the Office Online action URL.
*
* @private
* @param {string} apiHost - API Host
* @param {string} fileId - File ID
* @param {string} sharedLink - Shared link which may be a vanity URL
* @return {string} WOPI src URL
*/
setupWOPISrc(apiHost, fileId, sharedLink) {
// @TODO(jpress): add support for vanity URLs once WOPI API is updated
let wopiSrc = `${apiHost}/wopi/files/`;

if (sharedLink) {
const sharedName = sharedLink.split('/s/')[1];
if (sharedName) {
wopiSrc = `${wopiSrc}s_${sharedName}_f_`;
}
}

return `${wopiSrc}${fileId}`;
}

/**
* Creates the iframe element used for both the platform and webapp setup.
*
* @private
* @return {HTMLElement} Iframe element
*/
createIframeElement() {
const iframeEl = document.createElement('iframe');
iframeEl.setAttribute('width', '100%');
iframeEl.setAttribute('height', '100%');
iframeEl.setAttribute('frameborder', 0);
iframeEl.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');

if (this.platformSetup) {
iframeEl.setAttribute('allowfullscreen', 'true');
iframeEl.name = OFFICE_ONLINE_IFRAME_NAME;
iframeEl.id = OFFICE_ONLINE_IFRAME_NAME;
}

this.iframeEl.src = src;
return iframeEl;
}

/**
* Sets up the form that will be posted to the Office Online viewer. The form
* includes an access token, the token's time to live, and an action URL that
* Micrsoft uses to hit our WOPI endpoint.
*
* @private
* @param {string} apiHost - API host
* @param {string} fileId - File ID
* @param {string} sharedLink - Shared link which may be a vanity URL
* @param {string} locale - Locale
* @return {HTMLElement} Form element
*/
createFormElement(apiHost, fileId, sharedLink, locale) {
// Setting the action URL
const WOPISrc = this.setupWOPISrc(apiHost, fileId, sharedLink);
const origin = { origin: window.location.origin };
const formEl = this.containerEl.appendChild(document.createElement('form'));
// @TODO(jpress): add suport for iframe performance logging via App_LoadingStatus message
// We pass our origin in the sessionContext so that Microsoft will pass
// this to the checkFileInfo endpoint. From their we can set it as the
// origin for iframe postMessage communications.
formEl.setAttribute('action', `${EXCEL_ONLINE_EMBED_URL}?ui=${locale}&rs=${locale}&WOPISrc=${WOPISrc}&sc=${JSON.stringify(origin)}`);
formEl.setAttribute('method', 'POST');
formEl.setAttribute('target', OFFICE_ONLINE_IFRAME_NAME);

// Setting the token
const tokenInput = document.createElement('input');
tokenInput.setAttribute('name', 'access_token');
tokenInput.setAttribute('value', `${this.options.token}`);
tokenInput.setAttribute('type', 'hidden');

// Calculating and setting the time to live
const ttlInput = document.createElement('input');
ttlInput.setAttribute('name', 'access_token_TTL');
// Setting to 0 disables refresh messages from Microsoft
ttlInput.setAttribute('value', 0);
ttlInput.setAttribute('type', 'hidden');

formEl.appendChild(tokenInput);
formEl.appendChild(ttlInput);
return formEl;
}

/**
Expand Down
169 changes: 144 additions & 25 deletions src/lib/viewers/office/__tests__/OfficeViewer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { ICON_PRINT_CHECKMARK } from '../../../icons/icons';

const PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print
const PRINT_DIALOG_TIMEOUT_MS = 500;
const OFFICE_ONLINE_IFRAME_NAME = 'office-online-iframe';
const EXCEL_ONLINE_URL = 'https://excel.officeapps.live.com/x/_layouts/xlembed.aspx';

const sandbox = sinon.sandbox.create();
let office;
let stubs = {};
Expand All @@ -28,7 +31,18 @@ describe('lib/viewers/office/OfficeViewer', () => {
container: containerEl,
file: {
id: '123'
}
},
viewers: {
Office: {
shouldUsePlatformSetup: false
}
},
location: {
locale: 'en-US'
},
appHost: 'app.box.com',
apiHost: 'app.box.com',
token: 'token'
});
stubs = {
setupPDFUrl: sandbox.stub(office, 'setupPDFUrl')
Expand All @@ -48,24 +62,34 @@ describe('lib/viewers/office/OfficeViewer', () => {
if (office && typeof office.destroy === 'function') {
office.destroy();
}

stubs = null;
office = null;
});

describe('setup()', () => {
beforeEach(() => {
stubs.setupIframe = sandbox.stub(office, 'setupIframe');
stubs.initPrint = sandbox.stub(office, 'initPrint');
});

it('should set up the Office viewer', () => {
const testStubs = {
setupIframe: sandbox.stub(office, 'setupIframe'),
initPrint: sandbox.stub(office, 'initPrint')
};
office.setup();
expect(testStubs.setupIframe).to.be.called;
expect(testStubs.initPrint).to.be.called;
expect(stubs.setupIframe).to.be.called;
expect(stubs.initPrint).to.be.called;
expect(stubs.setupPDFUrl).to.be.called;
});
});

beforeEach(() => {
office.setup();
it('should not use the platform setup if the option is passed in', () => {
office.setup();
expect(office.platformSetup).to.be.false;
});

it('should use the platform setup if no option is passed in', () => {
office.options.viewers = {};
office.setup();
expect(office.platformSetup).to.be.true;
});
});

describe('destroy()', () => {
Expand Down Expand Up @@ -96,38 +120,133 @@ describe('lib/viewers/office/OfficeViewer', () => {

describe('setupIframe()', () => {
beforeEach(() => {
office.options.appHost = 'https://app.box.com';
stubs.createIframeElement = sandbox.spy(office, 'createIframeElement');
stubs.form = {
submit: sandbox.stub()
};

stubs.createFormElement = sandbox.stub(office, 'createFormElement').returns(stubs.form);
stubs.setupRunmodeURL = sandbox.stub(office, 'setupRunmodeURL').returns('src');
});

it('should initialize iframe element and set relevant attributes', () => {
expect(office.iframeEl.width).to.equal('100%');
expect(office.iframeEl.height).to.equal('100%');
expect(office.iframeEl.frameBorder).to.equal('0');
expect(office.iframeEl.sandbox.toString()).to.equal('allow-scripts allow-same-origin allow-forms allow-popups');
expect(office.loadTimeout).to.equal(120000);
it('should create the iframeEl', () => {
office.setupIframe();

expect(stubs.createIframeElement).to.be.called;
});

it('should load a xlsx file and set the file ID in src url on load event when the file is not a shared link', () => {
it('should finish setting up the iframe if using platform setup', () => {
office.platformSetup = true;
office.setupIframe();

const iframeEl = office.containerEl.querySelector(`#${OFFICE_ONLINE_IFRAME_NAME}`);
expect(iframeEl.name).to.equal(OFFICE_ONLINE_IFRAME_NAME);
expect(iframeEl.id).to.equal(OFFICE_ONLINE_IFRAME_NAME);
});

it('should setup and submit the form if using the platform setup', () => {
office.platformSetup = true;
office.setupIframe();
expect(office.iframeEl.src).to.equal('https://app.box.com/integrations/officeonline/openExcelOnlinePreviewer?fileId=123');

expect(stubs.createFormElement).to.be.calledWith(office.options.appHost, office.options.file.id, office.options.sharedLink, office.options.location.locale);
expect(stubs.form.submit).to.be.called;
});


it('should set the iframe source and sandbox attribute if not using the platform setup', () => {
office.setupIframe();

const iframeEl = office.containerEl.querySelector('iframe');
expect(iframeEl.src.endsWith('src')).to.be.true;
});
});
describe('setupRunmodeURL()', () => {
it('should load a xlsx file and set the file ID in src url on load event when the file is not a shared link', () => {
const src = office.setupRunmodeURL(office.options.appHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/integrations/officeonline/openExcelOnlinePreviewer?fileId=123');
});

it('should load a xlsx file and set the shared name in src url on load event when the file is a shared link', () => {
office.options.sharedLink = 'https://app.box.com/s/abcd';
office.setupIframe();
expect(office.iframeEl.src).to.equal('https://app.box.com/integrations/officeonline/openExcelOnlinePreviewer?s=abcd&fileId=123');
const src = office.setupRunmodeURL(office.options.appHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/integrations/officeonline/openExcelOnlinePreviewer?s=abcd&fileId=123');
});

it('should load a xlsx file and set the vanity name in src url on load event when the file is a vanity url without a subdomain', () => {
office.options.sharedLink = 'https://app.box.com/v/test';
office.setupIframe();
expect(office.iframeEl.src).to.equal('https://app.box.com/integrations/officeonline/openExcelOnlinePreviewer?v=test&vanity_subdomain=app&fileId=123');
const src = office.setupRunmodeURL(office.options.appHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/integrations/officeonline/openExcelOnlinePreviewer?v=test&vanity_subdomain=app&fileId=123');
});

it('should load a xlsx file and set the vanity name in src url on load event when the file is a vanity url with a subdomain', () => {
office.options.sharedLink = 'https://cloud.app.box.com/v/test';
office.setupIframe();
expect(office.iframeEl.src).to.equal('https://app.box.com/integrations/officeonline/openExcelOnlinePreviewer?v=test&vanity_subdomain=cloud&fileId=123');
const src = office.setupRunmodeURL(office.options.appHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/integrations/officeonline/openExcelOnlinePreviewer?v=test&vanity_subdomain=cloud&fileId=123');
});
});

describe('setupWOPISrc()', () => {
it('should append the file ID if there is no shared link', () => {
const src = office.setupWOPISrc(office.options.apiHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/wopi/files/123');
});

it('should append the shared name and file ID if there is a shared link', () => {
office.options.sharedLink = 'https://app.box.com/s/abcd';
const src = office.setupWOPISrc(office.options.apiHost, office.options.file.id, office.options.sharedLink);
expect(src).to.equal('app.box.com/wopi/files/s_abcd_f_123');
});

it('should not append the shared name if there is a vanity link', () => {
office.options.sharedLink = 'https://app.box.com/v/abcd';
const src = office.setupWOPISrc(office.options.apiHost, office.options.file.id, office.options.sharedLink);
expect(src.includes('s_')).to.be.false;
});
});

describe('createIframeElement()', () => {
it('should initialize iframe element and set relevant attributes', () => {
const iframeEl = office.createIframeElement();

expect(iframeEl.width).to.equal('100%');
expect(iframeEl.height).to.equal('100%');
expect(iframeEl.frameBorder).to.equal('0');
expect(iframeEl.getAttribute('sandbox')).to.equal('allow-scripts allow-same-origin allow-forms allow-popups');
});

it('should allow fullscreen if using the platform setup', () => {
office.platformSetup = true;
const iframeEl = office.createIframeElement();

expect(iframeEl.getAttribute('allowfullscreen')).to.equal('true');
});
});

describe('createFormElement()', () => {
beforeEach(() => {
stubs.setupWOPISrc = sandbox.stub(office, 'setupWOPISrc').returns('src');
stubs.sessionContext = JSON.stringify({ origin: window.location.origin });
stubs.formEl = office.createFormElement(office.options.apiHost, office.options.file.id, office.options.sharedLink, office.options.location.locale);
});

it('should correctly set the action URL', () => {
expect(stubs.formEl.getAttribute('action')).to.equal(`${EXCEL_ONLINE_URL}?ui=${office.options.location.locale}&rs=${office.options.location.locale}&WOPISrc=src&sc=${stubs.sessionContext}`);
expect(stubs.formEl.getAttribute('method')).to.equal('POST');
expect(stubs.formEl.getAttribute('target')).to.equal(OFFICE_ONLINE_IFRAME_NAME);
});

it('should correctly set the token', () => {
const tokenInputEl = stubs.formEl.querySelector('input[name="access_token"]');
expect(tokenInputEl.getAttribute('name')).to.equal('access_token');
expect(tokenInputEl.getAttribute('value')).to.equal('token');
expect(tokenInputEl.getAttribute('type')).to.equal('hidden');
});

it('should correctly set the token time to live', () => {
const tokenInputEl = stubs.formEl.querySelector('input[name="access_token_TTL"]');
expect(tokenInputEl.getAttribute('name')).to.equal('access_token_TTL');
expect(tokenInputEl.getAttribute('value')).to.not.equal(undefined);
expect(tokenInputEl.getAttribute('type')).to.equal('hidden');
});
});

Expand Down

0 comments on commit a247b9d

Please sign in to comment.