﻿/**
 * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

( function() {
	'use strict';

	/**
	 * JSONP communication.
	 *
	 * @private
	 * @singleton
	 * @class CKEDITOR.plugins.embedBase._jsonp
	 */
	var Jsonp = {
		/**
		 * Creates a `<script>` element and attaches it to the document `<body>`.
		 *
		 * @private
		 */
		_attachScript: function( url, errorCallback ) {
			// ATM we cannot use CKE scriptloader here, because it will make sure that script
			// with given URL is added only once.
			var script = new CKEDITOR.dom.element( 'script' );
			script.setAttribute( 'src', url );
			script.on( 'error', errorCallback );

			CKEDITOR.document.getBody().append( script );

			return script;
		},

		/**
		 * Sends a request using the JSONP technique.
		 *
		 * @param {CKEDITOR.template} urlTemplate The template of the URL to be requested. All properties
		 * passed in `urlParams` can be used, plus a `{callback}`, which represents a JSONP callback, must be defined.
		 * @param {Object} urlParams Parameters to be passed to the `urlTemplate`.
		 * @param {Function} callback
		 * @param {Function} [errorCallback]
		 * @returns {Object} The request object with a `cancel()` method.
		 */
		sendRequest: function( urlTemplate, urlParams, callback, errorCallback ) {
			var request = {};
			urlParams = urlParams || {};

			var callbackKey = CKEDITOR.tools.getNextNumber(),
				scriptElement;

			urlParams.callback = 'CKEDITOR._.jsonpCallbacks[' + callbackKey + ']';

			CKEDITOR._.jsonpCallbacks[ callbackKey ] = function( response ) {
				// On IEs scripts are sometimes loaded synchronously. It is bad for two reasons:
				// * nature of sendRequest() is unstable,
				// * scriptElement does not exist yet.
				setTimeout( function() {
					cleanUp();
					callback( response );
				} );
			};

			scriptElement = this._attachScript( urlTemplate.output( urlParams ), function() {
				cleanUp();
				errorCallback && errorCallback();
			} );

			request.cancel = cleanUp;

			function cleanUp() {
				if ( scriptElement ) {
					scriptElement.remove();
					delete CKEDITOR._.jsonpCallbacks[ callbackKey ];
					scriptElement = null;
				}
			}

			return request;
		}
	};

	CKEDITOR.plugins.add( 'embedbase', {
		lang: 'ar,az,bg,ca,cs,da,de,de-ch,en,en-au,eo,es,es-mx,eu,fr,gl,hr,hu,id,it,ja,ko,ku,lv,nb,nl,oc,pl,pt,pt-br,ro,ru,sk,sq,sv,tr,ug,uk,zh,zh-cn', // %REMOVE_LINE_CORE%
		requires: 'dialog,widget,notificationaggregator',

		onLoad: function() {
			CKEDITOR._.jsonpCallbacks = {};
		},

		init: function() {
			CKEDITOR.dialog.add( 'embedBase', this.path + 'dialogs/embedbase.js' );
		}
	} );

	/**
	 * Creates a new embed widget base definition. After other necessary properties are filled this definition
	 * may be {@link CKEDITOR.plugins.widget.repository#add registered} as a new, independent widget for
	 * embedding content.
	 *
	 * By default an embed widget is set up to work with [oEmbed providers](http://www.oembed.com/) using JSONP
	 * requests, such as [Iframely](https://iframely.com/) or [Noembed](https://noembed.com/). It can be,
	 * however, easily configured to use other providers and communication methods, including custom systems
	 * or local embed databases.
	 *
	 * See example usage of this method in:
	 *
	 * * [/plugins/embed/plugin.js](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/embed/plugin.js)
	 * * [/plugins/embedsemantic/plugin.js](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/embedsemantic/plugin.js)
	 *
	 * Note that both these plugins reuse the [dialog](https://github.com/ckeditor/ckeditor-dev/blob/master/plugins/embedbase/dialogs/embedbase.js)
	 * defined by the `embedbase` plugin. Integration of the asynchronous way of loading content with a dialog requires additional
	 * effort. Check the dialog's code for more details.
	 *
	 * @static
	 * @param {CKEDITOR.editor} editor
	 * @returns {CKEDITOR.plugins.embedBase.baseDefinition}
	 * @member CKEDITOR.plugins.embedBase
	 */
	function createWidgetBaseDefinition( editor ) {
		var aggregator,
			lang = editor.lang.embedbase;

		/**
		 * An embed widget base definition. It predefines a few {@link CKEDITOR.plugins.widget.definition widget definition}
		 * properties such as {@link #mask}, {@link #template} and {@link #pathName} and adds methods related to
		 * content embedding.
		 *
		 * To create a base definition use the {@link CKEDITOR.plugins.embedBase#createWidgetBaseDefinition} method.
		 *
		 * Note: For easier browsing of this class's API you can hide inherited method using the "Show" drop-down
		 * on the right-hand side.
		 *
		 * @abstract
		 * @class CKEDITOR.plugins.embedBase.baseDefinition
		 * @extends CKEDITOR.plugins.widget.definition
		 */
		return {
			mask: true,
			template: '<div></div>',
			pathName: lang.pathName,

			/**
			 * Response cache. This cache object will be shared between all instances of this widget.
			 *
			 * @private
			 */
			_cache: {},

			/**
			 * A regular expression to pre-validate URLs.
			 *
			 * See:
			 *
			 * * [https://iframely.com/docs/providers],
			 * * {@link #isUrlValid}.
			 */
			urlRegExp: /^((https?:)?\/\/|www\.)/i,

			/**
			 * The template used to generate the URL of the content provider. Content provider is a service
			 * which the embed widget will request in order to get an [oEmbed](http://www.oembed.com/) response that
			 * can be transformed into content which can be embedded in the editor.
			 *
			 * Example content providers are:
			 *
			 * * [Iframely](https://iframely.com/),
			 * * [Noembed](https://noembed.com/).
			 *
			 * Both Iframely and Noembed are **proxy** services which support **JSONP requests**, hence they are not limited by the
			 * same-origin policy. Unfortunately, usually oEmbed services exposed by real content providers
			 * like YouTube or Twitter do not support XHR with CORS or do not support oEmbed at all which makes it
			 * impossible or hard to get such content to be embedded in the editor. This problem is solved by proxy content providers
			 * like Iframely and Noembed.
			 *
			 * This property must be defined after creating an embed widget base definition.
			 *
			 * By default two values are passed to the template:
			 *
			 * * `{url}` &ndash; The URL of the resource to be embedded.
			 * * `{callback}` &ndash; The JSONP callback to be executed.
			 *
			 * Example value:
			 *
			 *		widgetDefinition.providerUrl = new CKEDITOR.template(
			 *			'//ckeditor.iframe.ly/api/oembed?url={url}&callback={callback}'
			 *		);
			 *
			 * @property {CKEDITOR.template} providerUrl
			 */

			init: function() {
				this.on( 'sendRequest', function( evt ) {
					this._sendRequest( evt.data );
				}, this, null, 999 );

				// Expose the widget in the dialog - needed to trigger loadContent() and do error handling.
				this.on( 'dialog', function( evt ) {
					evt.data.widget = this;
				}, this );

				this.on( 'handleResponse', function( evt ) {
					if ( evt.data.html ) {
						return;
					}

					var retHtml = this._responseToHtml( evt.data.url, evt.data.response );

					if ( retHtml !== null ) {
						evt.data.html = retHtml;
					} else {
						evt.data.errorMessage = 'unsupportedUrl';
						evt.cancel();
					}
				}, this, null, 999 );
			},

			/**
			 * Loads content for a given resource URL by requesting the {@link #providerUrl provider}.
			 *
			 * Usually widgets are controlled by the {@link CKEDITOR.plugins.widget#setData} method. However,
			 * loading content is an asynchronous operation due to client-server communication, and it would not
			 * be possible to pass callbacks to the {@link CKEDITOR.plugins.widget#setData} method so this new method
			 * is defined for embed widgets.
			 *
			 * This method fires two events that allow to customize widget behavior without changing its code:
			 *
			 * * {@link #sendRequest},
			 * * {@link #handleResponse} (if the request was successful).
			 *
			 * Note: This method is always asynchronous, even if the cache was hit.
			 *
			 * Example usage:
			 *
			 *		var url = 'https://twitter.com/reinmarpl/status/573118615274315776';
			 *		widget.loadContent( url, {
			 *			callback: function() {
			 *				// Success. It is a good time to save a snapshot.
			 *				editor.fire( 'saveSnapshot' );
			 *				console.log( widget.data.url ); // The above URL. It is only changed
			 *												// once the content is successfully loaded.
			 *			},
			 *
			 *			errorCallback: function( message ) {
			 *				editor.showNotification( widget.getErrorMessage( message, url ), 'warning' );
			 *			}
			 *		} );
			 *
			 * @param {String} url Resource URL to be embedded.
			 * @param {Object} opts
			 * @param {Function} [opts.callback] Callback called when content was successfully loaded into the editor.
			 * @param {Function} [opts.errorCallback] Callback called when an error occurred.
			 * @param {String} opts.errorCallback.messageTypeOrMessage See {@link #getErrorMessage}.
			 * @param {Boolean} [opts.noNotifications] Do not show notifications (useful when the dialog is open).
			 * @returns {CKEDITOR.plugins.embedBase.request}
			 */
			loadContent: function( url, opts ) {
				opts = opts || {};

				var that = this,
					cachedResponse = this._getCachedResponse( url ),
					request = {
						noNotifications: opts.noNotifications,
						url: url,
						callback: finishLoading,
						errorCallback: function( msg ) {
							that._handleError( request, msg );
							if ( opts.errorCallback ) {
								opts.errorCallback( msg );
							}
						}
					};

				if ( cachedResponse ) {
					// Keep the async nature (it caused a bug the very first day when the loadContent()
					// was synchronous when cache was hit :D).
					setTimeout( function() {
						finishLoading( cachedResponse );
					} );
					return;
				}

				if ( !opts.noNotifications ) {
					request.task = this._createTask();
				}

				// The execution will be followed by #sendRequest's listener.
				this.fire( 'sendRequest', request );

				function finishLoading( response ) {
					request.response = response;

					// Check if widget is still valid.
					if ( !that.editor.widgets.instances[ that.id ] ) {
						CKEDITOR.warn( 'embedbase-widget-invalid' );

						if ( request.task ) {
							request.task.done();
						}

						return;
					}

					if ( that._handleResponse( request ) ) {
						that._cacheResponse( url, response );
						if ( opts.callback ) {
							opts.callback();
						}
					}
				}

				return request;
			},

			/**
			 * Checks whether the URL is valid. Usually the content provider makes the final validation
			 * as only the provider knows what kind of URLs are accepted. However, to give the user some immediate feedback
			 * a synchronous validation is performed using the {@link #urlRegExp} pattern and the {@link #validateUrl} event.
			 *
			 * @param {String} url The URL to check.
			 * @returns {Boolean} Whether the URL is valid (supported).
			 */
			isUrlValid: function( url ) {
				return this.urlRegExp.test( url ) && this.fire( 'validateUrl', url ) !== false;
			},

			/**
			 * Generates an error message based on the message type (with a possible suffix) or
			 * the custom message template.
			 *
			 * This method is used when showing a notification or an alert (in a dialog) about an error.
			 * Usually it is used with an error type which is a string from the `editor.lang.embedbase` object.
			 *
			 * There are two error types available at the moment: `'unsupportedUrl'` and `'fetchingFailed'`.
			 * Additionally, both can be suffixed with `'Given'`. See the language entries to see the difference.
			 * Inside the dialog this method is used with a suffix and to generate a notification message it is
			 * used without a suffix.
			 *
			 * Additionally, a custom message may be passed and just like language entries, it can use the `{url}`
			 * placeholder.
			 *
			 * While {@link #handleResponse handling the response} you can set an error message or its type. It will
			 * be passed to this method later.
			 *
			 *		widget.on( 'handleResponse', function( evt ) {
			 *			if ( evt.data.response.type != 'rich' ) {
			 *				evt.data.errorMessage = '{url} cannot be embedded. Only rich type is supported.';
			 *				evt.cancel();
			 *
			 *				// Or:
			 *				evt.data.errorMessage = 'unsupportedUrl.';
			 *				evt.cancel();
			 *			}
			 *		} );
			 *
			 * If you need to display your own error:
			 *
			 *		editor.showNotification(
			 *			widget.getErrorMessage( '{url} cannot be embedded. Only rich type is supported.', wrongUrl )
			 *		);
			 *
			 * Or with a message type:
			 *
			 *		editor.showNotification(
			 *			widget.getErrorMessage( 'unsupportedUrl', wrongUrl )
			 *		);
			 *
			 * @param {String} messageTypeOrMessage
			 * @param {String} [url]
			 * @param {String} [suffix]
			 * @returns {String}
			 */
			getErrorMessage: function( messageTypeOrMessage, url, suffix ) {
				var message = editor.lang.embedbase[ messageTypeOrMessage + ( suffix || '' ) ];
				if ( !message ) {
					message = messageTypeOrMessage;
				}

				return new CKEDITOR.template( message ).output( { url: url || '' } );
			},

			/**
			 * Sends the request to the {@link #providerUrl provider} using
			 * the {@link CKEDITOR.plugins.embedBase._jsonp JSONP} technique.
			 *
			 * @private
			 * @param {CKEDITOR.plugins.embedBase.request} request
			 */
			_sendRequest: function( request ) {
				var that = this,
					jsonpRequest = Jsonp.sendRequest(
						this.providerUrl,
						{
							url: encodeURIComponent( request.url )
						},
						request.callback,
						function() {
							request.errorCallback( 'fetchingFailed' );
						}
					);

				request.cancel = function() {
					jsonpRequest.cancel();
					that.fire( 'requestCanceled', request );
				};
			},

			/**
			 * Handles the response of a successful request.
			 *
			 * Fires the {@link #handleResponse} event in order to convert the oEmbed response
			 * to HTML that can be embedded.
			 *
			 * If the response can be handled, the {@link #_setContent content is set}.
			 *
			 * @private
			 * @param {CKEDITOR.plugins.embedBase.request} request
			 * @returns {Boolean} Whether the response can be handled. Returns `false` if {@link #handleResponse}
			 * was canceled or the default listener could not convert oEmbed response into embeddable HTML.
			 */
			_handleResponse: function( request ) {
				var evtData = {
					url: request.url,
					html: '',
					response: request.response
				};

				if ( this.fire( 'handleResponse', evtData ) !== false ) {
					if ( request.task ) {
						request.task.done();
					}

					this._setContent( request.url, evtData.html );
					return true;
				} else {
					request.errorCallback( evtData.errorMessage );
					return false;
				}
			},

			/**
			 * Handles an error. An error can be caused either by a request failure or an unsupported
			 * oEmbed response type.
			 *
			 * @private
			 * @param {CKEDITOR.plugins.embedBase.request} request
			 * @param {String} messageTypeOrMessage See {@link #getErrorMessage}.
			 */
			_handleError: function( request, messageTypeOrMessage ) {
				if ( request.task ) {
					request.task.cancel();

					if ( !request.noNotifications ) {
						editor.showNotification( this.getErrorMessage( messageTypeOrMessage, request.url ), 'warning' );
					}
				}
			},

			/**
			 * Returns embeddable HTML for an oEmbed response if it is of the `photo`, `video` or `rich` type.
			 *
			 * @private
			 * @param {Object} response The oEmbed response.
			 * @returns {String/null} HTML string to be embedded or `null` if this response type is not supported.
			 */
			_responseToHtml: function( url, response ) {
				if ( response.type == 'photo' ) {
					return '<img src="' + CKEDITOR.tools.htmlEncodeAttr( response.url ) + '" ' +
						'alt="' + CKEDITOR.tools.htmlEncodeAttr( response.title || '' ) + '" style="max-width:100%;height:auto" />';
				} else if ( response.type == 'video' || response.type == 'rich' ) {
					// Embedded iframes are added to page's focus list. Adding negative tabindex attribute
					// removes their ability to be focused by user. (https://dev.ckeditor.com/ticket/14538)
					response.html = response.html.replace( /<iframe/g, '<iframe tabindex="-1"' );

					return response.html;
				}

				return null;
			},

			/**
			 * The very final step of {@link #loadContent content loading}. The `url` data property is changed
			 * and the content is embedded ({@link CKEDITOR.plugins.widget#element}'s HTML is set).
			 *
			 * @private
			 * @param {String} url The resource URL.
			 * @param {String} content HTML content to be embedded.
			 */
			_setContent: function( url, content ) {
				this.setData( 'url', url );
				this.element.setHtml( content );
			},

			/**
			 * Creates a notification aggregator task.
			 *
			 * @private
			 * @returns {CKEDITOR.plugins.notificationAggregator.task}
			 */
			_createTask: function() {
				if ( !aggregator || aggregator.isFinished() ) {
					aggregator = new CKEDITOR.plugins.notificationAggregator( editor, lang.fetchingMany, lang.fetchingOne );

					aggregator.on( 'finished', function() {
						aggregator.notification.hide();
					} );
				}

				return aggregator.createTask();
			},

			/**
			 * Caches the provider response.
			 *
			 * @private
			 * @param {String} url
			 * @param {Object} response
			 */
			_cacheResponse: function( url, response ) {
				this._cache[ url ] = response;
			},

			/**
			 * Returns the cached response.
			 *
			 * @private
			 * @param {String} url
			 * @returns {Object/undefined} Response or `undefined` if the cache was missed.
			 */
			_getCachedResponse: function( url ) {
				return this._cache[ url ];
			}
		};

		/**
		 * Fired by the {@link #isUrlValid} method. Cancel the event to make the URL invalid.
		 *
		 * @event validateUrl
		 * @param {String} data The URL being validated.
		 */

		/**
		 * Fired by the {@link #loadContent} method to dispatch a request to the provider.
		 * You can cancel this event and send the request using a different technique.
		 * By default, if the event is not stopped or canceled a request will be sent
		 * using the JSONP technique.
		 *
		 *		widget.on( 'sendRequest', function( evt ) {
		 *			var request = evt.data;
		 *
		 *			// Send the request using a technique of your choice (XHR with CORS for instance).
		 *			myApp.requestOembedProvider( request.url, function( err, response ) {
		 *				if ( err ) {
		 *					request.errorCallback( err );
		 *				} else {
		 *					request.callback( response );
		 *				}
		 *			} );
		 *
		 *			// Do not call other listeners, so the default behavior (JSONP request)
		 *			// will not be executed.
		 *			evt.stop();
		 *		} );
		 *
		 * @event sendRequest
		 * @param {CKEDITOR.plugins.embedBase.request} data
		 */

		/**
		 * Fired after receiving a response from the {@link #providerUrl provider}.
		 * This event listener job is to turn the oEmbed response to embeddable HTML by setting
		 * `evt.data.html`.
		 *
		 *		widget.on( 'handleReaponse', function( evt ) {
		 *			evt.data.html = customOembedToHtmlConverter( evt.data.response );
		 *		} );
		 *
		 * This event can also be canceled to indicate that the response cannot be handled. In such
		 * case the `evt.data.errorMessage` must be set (see {@link #getErrorMessage}).
		 *
		 *		widget.on( 'handleReaponse', function( evt ) {
		 *			if ( evt.data.response.type == 'photo' ) {
		 *				// Will display the editor.lang.embedbase.unsupportedUrl(Given) message.
		 *				evt.data.errorMessage = 'unsupportedUrl';
		 *				evt.cancel();
		 *			}
		 *		} );
		 *
		 * This event has a default late-listener (with a priority of `999`) that, if `evt.data.html` has not
		 * been set yet, will try to handle the response by using the {@link #_responseToHtml} method.
		 *
		 * @event handleResponse
		 * @param {Object} data
		 * @param {String} data.url The resource URL.
		 * @param {Object} data.response The oEmbed response.
		 * @param {String} [data.html=''] The HTML which will be embedded.
		 * @param {String} [data.errorMessage] The error message or message type (see {@link #getErrorMessage})
		 * that must be set if this event is canceled to indicate an unsupported oEmbed response.
		 */
	}

	/**
	 * Class representing the request object. It is created by the {@link CKEDITOR.plugins.embedBase.baseDefinition#loadContent}
	 * method and is passed to other methods and events of this class.
	 *
	 * @abstract
	 * @class CKEDITOR.plugins.embedBase.request
	 */

	/**
	 * The resource URL to be embedded (not the {@link CKEDITOR.plugins.embedBase.baseDefinition#providerUrl provider URL}).
	 *
	 * @property {String} url
	 */

	/**
	 * Success callback to be executed once a response to a request is received.
	 *
	 * @property {Function} [callback]
	 * @param {Object} response The response object.
	 */

	/**
	 * Callback executed in case of an error.
	 *
	 * @property {Function} [errorCallback]
	 * @param {String} messageTypeOrMessage See {@link CKEDITOR.plugins.embedBase.baseDefinition#getErrorMessage}.
	 */

	/**
	 * Task that should be resolved once the request is done.
	 *
	 * @property {CKEDITOR.plugins.notificationAggregator.task} [task]
	 */

	/**
	 * Response object. It is set once a response is received.
	 *
	 * @property {Object} [response]
	 */

	/**
	 * Cancels the request.
	 *
	 * @method cancel
	 */

	CKEDITOR.plugins.embedBase = {
		createWidgetBaseDefinition: createWidgetBaseDefinition,
		_jsonp: Jsonp
	};

} )();
