token.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module cloud-services-core/token
  7. */
  8. /* globals XMLHttpRequest, setInterval, clearInterval */
  9. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  10. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  11. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  12. const DEFAULT_OPTIONS = { refreshInterval: 3600000, autoRefresh: true };
  13. /**
  14. * Class representing the token used for communication with CKEditor Cloud Services.
  15. * Value of the token is retrieving from the specified URL and is refreshed every 1 hour by default.
  16. *
  17. * @mixes ObservableMixin
  18. */
  19. class Token {
  20. /**
  21. * Creates `Token` instance.
  22. * Method `init` should be called after using the constructor or use `create` method instead.
  23. *
  24. * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
  25. * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
  26. * @param {Object} options
  27. * @param {String} [options.initValue] Initial value of the token.
  28. * @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
  29. * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
  30. */
  31. constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
  32. if ( !tokenUrlOrRefreshToken ) {
  33. /**
  34. * A `tokenUrl` must be provided as the first constructor argument.
  35. *
  36. * @error token-missing-token-url
  37. */
  38. throw new CKEditorError(
  39. 'token-missing-token-url: A `tokenUrl` must be provided as the first constructor argument.',
  40. this
  41. );
  42. }
  43. /**
  44. * Value of the token.
  45. * The value of the token is null if `initValue` is not provided or `init` method was not called.
  46. * `create` method creates token with initialized value from url.
  47. *
  48. * @name value
  49. * @member {String} #value
  50. * @observable
  51. * @readonly
  52. */
  53. this.set( 'value', options.initValue );
  54. /**
  55. * Base refreshing function.
  56. *
  57. * @private
  58. * @member {String|Function} #_refresh
  59. */
  60. if ( typeof tokenUrlOrRefreshToken === 'function' ) {
  61. this._refresh = tokenUrlOrRefreshToken;
  62. } else {
  63. this._refresh = () => defaultRefreshToken( tokenUrlOrRefreshToken );
  64. }
  65. /**
  66. * @type {Object}
  67. * @private
  68. */
  69. this._options = Object.assign( {}, DEFAULT_OPTIONS, options );
  70. }
  71. /**
  72. * Initializes the token.
  73. *
  74. * @returns {Promise.<module:cloud-services-core/token~Token>}
  75. */
  76. init() {
  77. return new Promise( ( resolve, reject ) => {
  78. if ( this._options.autoRefresh ) {
  79. this._startRefreshing();
  80. }
  81. if ( !this.value ) {
  82. this.refreshToken()
  83. .then( resolve )
  84. .catch( reject );
  85. return;
  86. }
  87. resolve( this );
  88. } );
  89. }
  90. /**
  91. * Refresh token method. Useful in a method form as it can be override in tests.
  92. * @returns {Promise.<String>}
  93. */
  94. refreshToken() {
  95. return this._refresh()
  96. .then( value => this.set( 'value', value ) )
  97. .then( () => this );
  98. }
  99. /**
  100. * Destroys token instance. Stops refreshing.
  101. */
  102. destroy() {
  103. this._stopRefreshing();
  104. }
  105. /**
  106. * Starts value refreshing every `refreshInterval` time.
  107. *
  108. * @protected
  109. */
  110. _startRefreshing() {
  111. this._refreshInterval = setInterval( () => this.refreshToken(), this._options.refreshInterval );
  112. }
  113. /**
  114. * Stops value refreshing.
  115. *
  116. * @protected
  117. */
  118. _stopRefreshing() {
  119. clearInterval( this._refreshInterval );
  120. }
  121. /**
  122. * Creates a initialized {@link module:cloud-services-core/token~Token} instance.
  123. *
  124. * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
  125. * value is a function it has to match the {@link module:cloud-services-core/token~refreshToken} interface.
  126. * @param {Object} options
  127. * @param {String} [options.initValue] Initial value of the token.
  128. * @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
  129. * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
  130. * @returns {Promise.<module:cloud-services-core/token~Token>}
  131. */
  132. static create( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
  133. const token = new Token( tokenUrlOrRefreshToken, options );
  134. return token.init();
  135. }
  136. }
  137. mix( Token, ObservableMixin );
  138. /**
  139. * This function is called in a defined interval by the {@link ~Token} class. It also can be invoked manually.
  140. * It should return a promise, which resolves with the new token value.
  141. * If any error occurs it should return a rejected promise with an error message.
  142. *
  143. * @function refreshToken
  144. * @returns {Promise.<String>}
  145. */
  146. /**
  147. * @private
  148. * @param {String} tokenUrl
  149. */
  150. function defaultRefreshToken( tokenUrl ) {
  151. return new Promise( ( resolve, reject ) => {
  152. const xhr = new XMLHttpRequest();
  153. xhr.open( 'GET', tokenUrl );
  154. xhr.addEventListener( 'load', () => {
  155. const statusCode = xhr.status;
  156. const xhrResponse = xhr.response;
  157. if ( statusCode < 200 || statusCode > 299 ) {
  158. /**
  159. * Cannot download new token from the provided url.
  160. *
  161. * @error token-cannot-download-new-token
  162. */
  163. return reject(
  164. new CKEditorError( 'token-cannot-download-new-token: Cannot download new token from the provided url.', null )
  165. );
  166. }
  167. return resolve( xhrResponse );
  168. } );
  169. xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) );
  170. xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) );
  171. xhr.send();
  172. } );
  173. }
  174. export default Token;