Skip to content

Commit

Permalink
Merge pull request #11999 from ckeditor/cf/4611-handle-no-navigator-api
Browse files Browse the repository at this point in the history
Internal (utils): Wrapped accessing browser's API in try/catch blocks to safely initialize editor in environments without browser's APIs.
  • Loading branch information
arkflpc authored Aug 23, 2022
2 parents 38e81b5 + affae75 commit 64c47c7
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 21 deletions.
19 changes: 18 additions & 1 deletion packages/ckeditor5-utils/src/dom/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,21 @@
*
* console.log( global.window.innerWidth );
*/
export default { window, document };

let global: { window: Window & typeof globalThis; document: Document };

// In some environments window and document API might not be available.
try {
global = { window, document };
} catch ( e ) {
// It's not possible to mock a window object to simulate lack of a window object without writing extremely convoluted code.
/* istanbul ignore next */

// Let's cast it to not change module's API.
// We only handle this so loading editor in environments without window and document doesn't fail.
// For better DX we shouldn't introduce mixed types and require developers to check the type manually.
// This module should not be used on purpose in any environment outside browser.
global = { window: {} as any, document: {} as any };
}

export default global;
17 changes: 16 additions & 1 deletion packages/ckeditor5-utils/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@
* @module utils/env
*/

const userAgent = navigator.userAgent.toLowerCase();
/**
* Safely returns `userAgent` from browser's navigator API in a lower case.
* If navigator API is not available it will return an empty string.
*
* @returns {String}
*/
export function getUserAgent( ): string {
// In some environments navigator API might not be available.
try {
return navigator.userAgent.toLowerCase();
} catch ( e ) {
return '';
}
}

const userAgent = getUserAgent();

/**
* A namespace containing environment and browser information.
Expand Down
36 changes: 18 additions & 18 deletions packages/ckeditor5-utils/src/translation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals window */
/* eslint-disable no-var */

/**
* @module utils/translation-service
*/

import CKEditorError from './ckeditorerror';
import global from './dom/global';

declare global {
var CKEDITOR_TRANSLATIONS: {
Expand All @@ -22,8 +22,8 @@ declare global {
}

/* istanbul ignore else */
if ( !window.CKEDITOR_TRANSLATIONS ) {
window.CKEDITOR_TRANSLATIONS = {};
if ( !global.window.CKEDITOR_TRANSLATIONS ) {
global.window.CKEDITOR_TRANSLATIONS = {};
}

/**
Expand Down Expand Up @@ -78,15 +78,15 @@ if ( !window.CKEDITOR_TRANSLATIONS ) {
* the one below:
*
* function addTranslations( language, translations, getPluralForm ) {
* if ( !window.CKEDITOR_TRANSLATIONS ) {
* window.CKEDITOR_TRANSLATIONS = {};
* if ( !global.window.CKEDITOR_TRANSLATIONS ) {
* global.window.CKEDITOR_TRANSLATIONS = {};
* }
* if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) {
* window.CKEDITOR_TRANSLATIONS[ language ] = {};
* if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) {
* global.window.CKEDITOR_TRANSLATIONS[ language ] = {};
* }
*
* const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ];
* const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ];
*
* languageTranslations.dictionary = languageTranslations.dictionary || {};
* languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
Expand All @@ -106,11 +106,11 @@ export function add(
translations: { readonly [ messageId: string ]: string | readonly string[] },
getPluralForm?: ( n: number ) => number
): void {
if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) {
window.CKEDITOR_TRANSLATIONS[ language ] = {} as any;
if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) {
global.window.CKEDITOR_TRANSLATIONS[ language ] = {} as any;
}

const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ];
const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ];

languageTranslations.dictionary = languageTranslations.dictionary || {};
languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
Expand Down Expand Up @@ -166,7 +166,7 @@ export function _translate( language: string, message: Message, quantity: number
if ( numberOfLanguages === 1 ) {
// Override the language to the only supported one.
// This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization.
language = Object.keys( window.CKEDITOR_TRANSLATIONS )[ 0 ];
language = Object.keys( global.window.CKEDITOR_TRANSLATIONS )[ 0 ];
}

const messageId = message.id || message.string;
Expand All @@ -180,8 +180,8 @@ export function _translate( language: string, message: Message, quantity: number
return message.string;
}

const dictionary = window.CKEDITOR_TRANSLATIONS[ language ].dictionary;
const getPluralForm = window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 );
const dictionary = global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary;
const getPluralForm = global.window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 );
const translation = dictionary[ messageId ];

if ( typeof translation === 'string' ) {
Expand All @@ -200,19 +200,19 @@ export function _translate( language: string, message: Message, quantity: number
* @protected
*/
export function _clear(): void {
window.CKEDITOR_TRANSLATIONS = {};
global.window.CKEDITOR_TRANSLATIONS = {};
}

// Checks whether the dictionary exists and translation in that dictionary exists.
function hasTranslation( language: string, messageId: string ): boolean {
return (
!!window.CKEDITOR_TRANSLATIONS[ language ] &&
!!window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
!!global.window.CKEDITOR_TRANSLATIONS[ language ] &&
!!global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
);
}

function getNumberOfLanguages(): number {
return Object.keys( window.CKEDITOR_TRANSLATIONS ).length;
return Object.keys( global.window.CKEDITOR_TRANSLATIONS ).length;
}

/**
Expand Down
24 changes: 23 additions & 1 deletion packages/ckeditor5-utils/tests/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import env, {
isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink
isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink, getUserAgent
} from '../src/env';

import global from '../src/dom/global';
Expand Down Expand Up @@ -286,4 +286,26 @@ describe( 'Env', () => {
}
} );
} );

describe( 'getUserAgent()', () => {
it( 'should return user agent in lower case', () => {
sinon.stub( global.window.navigator, 'userAgent' ).value( 'CKBrowser' );

expect( getUserAgent() ).to.equal( 'ckbrowser' );
} );

it( 'should return empty string if navigator API is unavailable', () => {
sinon.stub( global.window, 'navigator' ).value( undefined );

expect( getUserAgent() ).to.equal( '' );
} );

it( 'should not throw an error if navigator API is unavailable', () => {
sinon.stub( global.window, 'navigator' ).value( undefined );

expect( () => {
getUserAgent();
} ).to.not.throw();
} );
} );
} );

0 comments on commit 64c47c7

Please sign in to comment.