PHP Classes

File: core/assets/plugins/ckeditor/core/filter.js

Recommend this page to a friend!
  Classes of No name   RT Adminlte   core/assets/plugins/ckeditor/core/filter.js   Download  
File: core/assets/plugins/ckeditor/core/filter.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: RT Adminlte
Generate layout and menus for Adminlte
Author: By
Last change:
Date: 7 years ago
Size: 82,842 bytes
 

Contents

Class file image Download
/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or http://ckeditor.com/license */ ( function() { 'use strict'; var DTD = CKEDITOR.dtd, // processElement flag - means that element has been somehow modified. FILTER_ELEMENT_MODIFIED = 1, // processElement flag - meaning explained in CKEDITOR.FILTER_SKIP_TREE doc. FILTER_SKIP_TREE = 2, copy = CKEDITOR.tools.copy, trim = CKEDITOR.tools.trim, TEST_VALUE = 'cke-test', enterModeTags = [ '', 'p', 'br', 'div' ]; /** * A flag indicating that the current element and all its ancestors * should not be filtered. * * See {@link CKEDITOR.filter#addElementCallback} for more details. * * @since 4.4 * @readonly * @property {Number} [=2] * @member CKEDITOR */ CKEDITOR.FILTER_SKIP_TREE = FILTER_SKIP_TREE; /** * Highly configurable class which implements input data filtering mechanisms * and core functions used for the activation of editor features. * * A filter instance is always available under the {@link CKEDITOR.editor#filter} * property and is used by the editor in its core features like filtering input data, * applying data transformations, validating whether a feature may be enabled for * the current setup. It may be configured in two ways: * * * By the user, with the {@link CKEDITOR.config#allowedContent} setting. * * Automatically, by loaded features (toolbar items, commands, etc.). * * In both cases additional allowed content rules may be added by * setting the {@link CKEDITOR.config#extraAllowedContent} * configuration option. * * **Note**: Filter rules will be extended with the following elements * depending on the {@link CKEDITOR.config#enterMode} and * {@link CKEDITOR.config#shiftEnterMode} settings: * * * `'p'` &ndash; for {@link CKEDITOR#ENTER_P}, * * `'div'` &ndash; for {@link CKEDITOR#ENTER_DIV}, * * `'br'` &ndash; for {@link CKEDITOR#ENTER_BR}. * * **Read more** about the Advanced Content Filter in [guides](#!/guide/dev_advanced_content_filter). * * Filter may also be used as a standalone instance by passing * {@link CKEDITOR.filter.allowedContentRules} instead of {@link CKEDITOR.editor} * to the constructor: * * var filter = new CKEDITOR.filter( 'b' ); * * filter.check( 'b' ); // -> true * filter.check( 'i' ); // -> false * filter.allow( 'i' ); * filter.check( 'i' ); // -> true * * @since 4.1 * @class * @constructor Creates a filter class instance. * @param {CKEDITOR.editor/CKEDITOR.filter.allowedContentRules} editorOrRules */ CKEDITOR.filter = function( editorOrRules ) { /** * Whether custom {@link CKEDITOR.config#allowedContent} was set. * * This property does not apply to the standalone filter. * * @readonly * @property {Boolean} customConfig */ /** * Array of rules added by the {@link #allow} method (including those * loaded from {@link CKEDITOR.config#allowedContent} and * {@link CKEDITOR.config#extraAllowedContent}). * * Rules in this array are in unified allowed content rules format. * * This property is useful for debugging issues with rules string parsing * or for checking which rules were automatically added by editor features. * * @readonly */ this.allowedContent = []; /** * Array of rules added by the {@link #disallow} method (including those * loaded from {@link CKEDITOR.config#disallowedContent}). * * Rules in this array are in unified disallowed content rules format. * * This property is useful for debugging issues with rules string parsing * or for checking which rules were automatically added by editor features. * * @since 4.4 * @readonly */ this.disallowedContent = []; /** * Array of element callbacks. See {@link #addElementCallback}. * * @readonly * @property {Function[]} [=null] */ this.elementCallbacks = null; /** * Whether the filter is disabled. * * To disable the filter, set {@link CKEDITOR.config#allowedContent} to `true` * or use the {@link #disable} method. * * @readonly */ this.disabled = false; /** * Editor instance if not a standalone filter. * * @readonly * @property {CKEDITOR.editor} [=null] */ this.editor = null; /** * Filter's unique id. It can be used to find filter instance in * {@link CKEDITOR.filter#instances CKEDITOR.filter.instance} object. * * @since 4.3 * @readonly * @property {Number} id */ this.id = CKEDITOR.tools.getNextNumber(); this._ = { // Optimized allowed content rules. allowedRules: { elements: {}, generic: [] }, // Optimized disallowed content rules. disallowedRules: { elements: {}, generic: [] }, // Object: element name => array of transformations groups. transformations: {}, cachedTests: {} }; // Register filter instance. CKEDITOR.filter.instances[ this.id ] = this; if ( editorOrRules instanceof CKEDITOR.editor ) { var editor = this.editor = editorOrRules; this.customConfig = true; var allowedContent = editor.config.allowedContent; // Disable filter completely by setting config.allowedContent = true. if ( allowedContent === true ) { this.disabled = true; return; } if ( !allowedContent ) this.customConfig = false; this.allow( allowedContent, 'config', 1 ); this.allow( editor.config.extraAllowedContent, 'extra', 1 ); // Enter modes should extend filter rules (ENTER_P adds 'p' rule, etc.). this.allow( enterModeTags[ editor.enterMode ] + ' ' + enterModeTags[ editor.shiftEnterMode ], 'default', 1 ); this.disallow( editor.config.disallowedContent ); } // Rules object passed in editorOrRules argument - initialize standalone filter. else { this.customConfig = false; this.allow( editorOrRules, 'default', 1 ); } }; /** * Object containing all filter instances stored under their * {@link #id} properties. * * var filter = new CKEDITOR.filter( 'p' ); * filter === CKEDITOR.filter.instances[ filter.id ]; * * @since 4.3 * @static * @property instances */ CKEDITOR.filter.instances = {}; CKEDITOR.filter.prototype = { /** * Adds allowed content rules to the filter. * * Read about rules formats in [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). * * // Add a basic rule for custom image feature (e.g. 'MyImage' button). * editor.filter.allow( 'img[!src,alt]', 'MyImage' ); * * // Add rules for two header styles allowed by 'HeadersCombo'. * var header1Style = new CKEDITOR.style( { element: 'h1' } ), * header2Style = new CKEDITOR.style( { element: 'h2' } ); * editor.filter.allow( [ header1Style, header2Style ], 'HeadersCombo' ); * * @param {CKEDITOR.filter.allowedContentRules} newRules Rule(s) to be added. * @param {String} [featureName] Name of a feature that allows this content (most often plugin/button/command name). * @param {Boolean} [overrideCustom] By default this method will reject any rules * if {@link CKEDITOR.config#allowedContent} is defined to avoid overriding it. * Pass `true` to force rules addition. * @returns {Boolean} Whether the rules were accepted. */ allow: function( newRules, featureName, overrideCustom ) { // Check arguments and constraints. Clear cache. if ( !beforeAddingRule( this, newRules, overrideCustom ) ) return false; var i, ret; if ( typeof newRules == 'string' ) newRules = parseRulesString( newRules ); else if ( newRules instanceof CKEDITOR.style ) { // If style has the cast method defined, use it and abort. if ( newRules.toAllowedContentRules ) return this.allow( newRules.toAllowedContentRules( this.editor ), featureName, overrideCustom ); newRules = convertStyleToRules( newRules ); } else if ( CKEDITOR.tools.isArray( newRules ) ) { for ( i = 0; i < newRules.length; ++i ) ret = this.allow( newRules[ i ], featureName, overrideCustom ); return ret; // Return last status. } addAndOptimizeRules( this, newRules, featureName, this.allowedContent, this._.allowedRules ); return true; }, /** * Applies this filter to passed {@link CKEDITOR.htmlParser.fragment} or {@link CKEDITOR.htmlParser.element}. * The result of filtering is a DOM tree without disallowed content. * * // Create standalone filter passing 'p' and 'b' elements. * var filter = new CKEDITOR.filter( 'p b' ), * // Parse HTML string to pseudo DOM structure. * fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p><b>foo</b> <i>bar</i></p>' ), * writer = new CKEDITOR.htmlParser.basicWriter(); * * filter.applyTo( fragment ); * fragment.writeHtml( writer ); * writer.getHtml(); // -> '<p><b>foo</b> bar</p>' * * @param {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} fragment Node to be filtered. * @param {Boolean} [toHtml] Set to `true` if the filter is used together with {@link CKEDITOR.htmlDataProcessor#toHtml}. * @param {Boolean} [transformOnly] If set to `true` only transformations will be applied. Content * will not be filtered with allowed content rules. * @param {Number} [enterMode] Enter mode used by the filter when deciding how to strip disallowed element. * Defaults to {@link CKEDITOR.editor#activeEnterMode} for a editor's filter or to {@link CKEDITOR#ENTER_P} for standalone filter. * @returns {Boolean} Whether some part of the `fragment` was removed by the filter. */ applyTo: function( fragment, toHtml, transformOnly, enterMode ) { if ( this.disabled ) return false; var that = this, toBeRemoved = [], protectedRegexs = this.editor && this.editor.config.protectedSource, processRetVal, isModified = false, filterOpts = { doFilter: !transformOnly, doTransform: true, doCallbacks: true, toHtml: toHtml }; // Filter all children, skip root (fragment or editable-like wrapper used by data processor). fragment.forEach( function( el ) { if ( el.type == CKEDITOR.NODE_ELEMENT ) { // Do not filter element with data-cke-filter="off" and all their descendants. if ( el.attributes[ 'data-cke-filter' ] == 'off' ) return false; // (#10260) Don't touch elements like spans with data-cke-* attribute since they're // responsible e.g. for placing markers, bookmarks, odds and stuff. // We love 'em and we don't wanna lose anything during the filtering. // '|' is to avoid tricky joints like data-="foo" + cke-="bar". Yes, they're possible. // // NOTE: data-cke-* assigned elements are preserved only when filter is used with // htmlDataProcessor.toHtml because we don't want to protect them when outputting data // (toDataFormat). if ( toHtml && el.name == 'span' && ~CKEDITOR.tools.objectKeys( el.attributes ).join( '|' ).indexOf( 'data-cke-' ) ) return; processRetVal = processElement( that, el, toBeRemoved, filterOpts ); if ( processRetVal & FILTER_ELEMENT_MODIFIED ) isModified = true; else if ( processRetVal & FILTER_SKIP_TREE ) return false; } else if ( el.type == CKEDITOR.NODE_COMMENT && el.value.match( /^\{cke_protected\}(?!\{C\})/ ) ) { if ( !processProtectedElement( that, el, protectedRegexs, filterOpts ) ) toBeRemoved.push( el ); } }, null, true ); if ( toBeRemoved.length ) isModified = true; var node, element, check, toBeChecked = [], enterTag = enterModeTags[ enterMode || ( this.editor ? this.editor.enterMode : CKEDITOR.ENTER_P ) ], parentDtd; // Remove elements in reverse order - from leaves to root, to avoid conflicts. while ( ( node = toBeRemoved.pop() ) ) { if ( node.type == CKEDITOR.NODE_ELEMENT ) removeElement( node, enterTag, toBeChecked ); // This is a comment securing rejected element - remove it completely. else node.remove(); } // Check elements that have been marked as possibly invalid. while ( ( check = toBeChecked.pop() ) ) { element = check.el; // Element has been already removed. if ( !element.parent ) continue; // Handle custom elements as inline elements (#12683). parentDtd = DTD[ element.parent.name ] || DTD.span; switch ( check.check ) { // Check if element itself is correct. case 'it': // Check if element included in $removeEmpty has no children. if ( DTD.$removeEmpty[ element.name ] && !element.children.length ) removeElement( element, enterTag, toBeChecked ); // Check if that is invalid element. else if ( !validateElement( element ) ) removeElement( element, enterTag, toBeChecked ); break; // Check if element is in correct context. If not - remove element. case 'el-up': // Check if e.g. li is a child of body after ul has been removed. if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) removeElement( element, enterTag, toBeChecked ); break; // Check if element is in correct context. If not - remove parent. case 'parent-down': if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) removeElement( element.parent, enterTag, toBeChecked ); break; } } return isModified; }, /** * Checks whether a {@link CKEDITOR.feature} can be enabled. Unlike {@link #addFeature}, * this method always checks the feature, even when the default configuration * for {@link CKEDITOR.config#allowedContent} is used. * * // TODO example * * @param {CKEDITOR.feature} feature The feature to be tested. * @returns {Boolean} Whether this feature can be enabled. */ checkFeature: function( feature ) { if ( this.disabled ) return true; if ( !feature ) return true; // Some features may want to register other features. // E.g. a button may return a command bound to it. if ( feature.toFeature ) feature = feature.toFeature( this.editor ); return !feature.requiredContent || this.check( feature.requiredContent ); }, /** * Disables Advanced Content Filter. * * This method is meant to be used by plugins which are not * compatible with the filter and in other cases in which the filter * has to be disabled during the initialization phase or runtime. * * In other cases the filter can be disabled by setting * {@link CKEDITOR.config#allowedContent} to `true`. */ disable: function() { this.disabled = true; }, /** * Adds disallowed content rules to the filter. * * Read about rules formats in the [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). * * // Disallow all styles on the image elements. * editor.filter.disallow( 'img{*}' ); * * // Disallow all span and div elements. * editor.filter.disallow( 'span div' ); * * @since 4.4 * @param {CKEDITOR.filter.disallowedContentRules} newRules Rule(s) to be added. */ disallow: function( newRules ) { // Check arguments and constraints. Clear cache. // Note: we pass true in the 3rd argument, because disallow() should never // be blocked by custom configuration. if ( !beforeAddingRule( this, newRules, true ) ) return false; if ( typeof newRules == 'string' ) newRules = parseRulesString( newRules ); addAndOptimizeRules( this, newRules, null, this.disallowedContent, this._.disallowedRules ); return true; }, /** * Adds an array of {@link CKEDITOR.feature} content forms. All forms * will then be transformed to the first form which is allowed by the filter. * * editor.filter.allow( 'i; span{!font-style}' ); * editor.filter.addContentForms( [ * 'em', * 'i', * [ 'span', function( el ) { * return el.styles[ 'font-style' ] == 'italic'; * } ] * ] ); * // Now <em> and <span style="font-style:italic"> will be replaced with <i> * // because this is the first allowed form. * // <span> is allowed too, but it is the last form and * // additionaly, the editor cannot transform an element based on * // the array+function form). * * This method is used by the editor to register {@link CKEDITOR.feature#contentForms} * when adding a feature with {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. * * @param {Array} forms The content forms of a feature. */ addContentForms: function( forms ) { if ( this.disabled ) return; if ( !forms ) return; var i, form, transfGroups = [], preferredForm; // First, find preferred form - this is, first allowed. for ( i = 0; i < forms.length && !preferredForm; ++i ) { form = forms[ i ]; // Check only strings and styles - array format isn't supported by #check(). if ( ( typeof form == 'string' || form instanceof CKEDITOR.style ) && this.check( form ) ) preferredForm = form; } // This feature doesn't have preferredForm, so ignore it. if ( !preferredForm ) return; for ( i = 0; i < forms.length; ++i ) transfGroups.push( getContentFormTransformationGroup( forms[ i ], preferredForm ) ); this.addTransformations( transfGroups ); }, /** * Adds a callback which will be executed on every element * that the filter reaches when filtering, before the element is filtered. * * By returning {@link CKEDITOR#FILTER_SKIP_TREE} it is possible to * skip filtering of the current element and all its ancestors. * * editor.filter.addElementCallback( function( el ) { * if ( el.hasClass( 'protected' ) ) * return CKEDITOR.FILTER_SKIP_TREE; * } ); * * **Note:** At this stage the element passed to the callback does not * contain `attributes`, `classes`, and `styles` properties which are available * temporarily on later stages of the filtering process. Therefore you need to * use the pure {@link CKEDITOR.htmlParser.element} interface. * * @since 4.4 * @param {Function} callback The callback to be executed. */ addElementCallback: function( callback ) { // We want to keep it a falsy value, to speed up finding whether there are any callbacks. if ( !this.elementCallbacks ) this.elementCallbacks = []; this.elementCallbacks.push( callback ); }, /** * Checks whether a feature can be enabled for the HTML restrictions in place * for the current CKEditor instance, based on the HTML code the feature might * generate and the minimal HTML code the feature needs to be able to generate. * * // TODO example * * @param {CKEDITOR.feature} feature * @returns {Boolean} Whether this feature can be enabled. */ addFeature: function( feature ) { if ( this.disabled ) return true; if ( !feature ) return true; // Some features may want to register other features. // E.g. a button may return a command bound to it. if ( feature.toFeature ) feature = feature.toFeature( this.editor ); // If default configuration (will be checked inside #allow()), // then add allowed content rules. this.allow( feature.allowedContent, feature.name ); this.addTransformations( feature.contentTransformations ); this.addContentForms( feature.contentForms ); // If custom configuration or any DACRs, then check if required content is allowed. if ( feature.requiredContent && ( this.customConfig || this.disallowedContent.length ) ) return this.check( feature.requiredContent ); return true; }, /** * Adds an array of content transformation groups. One group * may contain many transformation rules, but only the first * matching rule in a group is executed. * * A single transformation rule is an object with four properties: * * * `check` (optional) &ndash; if set and {@link CKEDITOR.filter} does * not accept this {@link CKEDITOR.filter.contentRule}, this transformation rule * will not be executed (it does not *match*). This value is passed * to {@link #check}. * * `element` (optional) &ndash; this string property tells the filter on which * element this transformation can be run. It is optional, because * the element name can be obtained from `check` (if it is a String format) * or `left` (if it is a {@link CKEDITOR.style} instance). * * `left` (optional) &ndash; a function accepting an element or a {@link CKEDITOR.style} * instance verifying whether the transformation should be * executed on this specific element. If it returns `false` or if an element * does not match this style, this transformation rule does not *match*. * * `right` &ndash; a function accepting an element and {@link CKEDITOR.filter.transformationsTools} * or a string containing the name of the {@link CKEDITOR.filter.transformationsTools} method * that should be called on an element. * * A shorthand format is also available. A transformation rule can be defined by * a single string `'check:right'`. The string before `':'` will be used as * the `check` property and the second part as the `right` property. * * Transformation rules can be grouped. The filter will try to apply * the first rule in a group. If it *matches*, the filter will ignore subsequent rules and * will move to the next group. If it does not *match*, the next rule will be checked. * * Examples: * * editor.filter.addTransformations( [ * // First group. * [ * // First rule. If table{width} is allowed, it * // executes {@link CKEDITOR.filter.transformationsTools#sizeToStyle} on a table element. * 'table{width}: sizeToStyle', * // Second rule should not be executed if the first was. * 'table[width]: sizeToAttribute' * ], * // Second group. * [ * // This rule will add the foo="1" attribute to all images that * // do not have it. * { * element: 'img', * left: function( el ) { * return !el.attributes.foo; * }, * right: function( el, tools ) { * el.attributes.foo = '1'; * } * } * ] * ] ); * * // Case 1: * // config.allowedContent = 'table{height,width}; tr td'. * // * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' * * // Case 2: * // config.allowedContent = 'table[height,width]; tr td'. * // * // '<table style="height:100px; width:200px">...</table>' -> '<table height="100" width="200">...</table>' * // '<table height="100" width="200">...</table>' -> '<table height="100" width="200"">...</table>' * * // Case 3: * // config.allowedContent = 'table{width,height}[height,width]; tr td'. * // * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' * // * // Note: Both forms are allowed (size set by style and by attributes), but only * // the first transformation is applied &mdash; the size is always transformed to a style. * // This is because only the first transformation matching allowed content rules is applied. * * This method is used by the editor to add {@link CKEDITOR.feature#contentTransformations} * when adding a feature by {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. * * @param {Array} transformations */ addTransformations: function( transformations ) { if ( this.disabled ) return; if ( !transformations ) return; var optimized = this._.transformations, group, i; for ( i = 0; i < transformations.length; ++i ) { group = optimizeTransformationsGroup( transformations[ i ] ); if ( !optimized[ group.name ] ) optimized[ group.name ] = []; optimized[ group.name ].push( group.rules ); } }, /** * Checks whether the content defined in the `test` argument is allowed * by this filter. * * If `strictCheck` is set to `false` (default value), this method checks * if all parts of the `test` (styles, attributes, and classes) are * accepted by the filter. If `strictCheck` is set to `true`, the test * must also contain the required attributes, styles, and classes. * * For example: * * // Rule: 'img[!src,alt]'. * filter.check( 'img[alt]' ); // -> true * filter.check( 'img[alt]', true, true ); // -> false * * Second `check()` call returned `false` because `src` is required. * * **Note:** The `test` argument is of {@link CKEDITOR.filter.contentRule} type, which is * a limited version of {@link CKEDITOR.filter.allowedContentRules}. Read more about it * in the {@link CKEDITOR.filter.contentRule}'s documentation. * * @param {CKEDITOR.filter.contentRule} test * @param {Boolean} [applyTransformations=true] Whether to use registered transformations. * @param {Boolean} [strictCheck] Whether the filter should check if an element with exactly * these properties is allowed. * @returns {Boolean} Returns `true` if the content is allowed. */ check: function( test, applyTransformations, strictCheck ) { if ( this.disabled ) return true; // If rules are an array, expand it and return the logical OR value of // the rules. if ( CKEDITOR.tools.isArray( test ) ) { for ( var i = test.length ; i-- ; ) { if ( this.check( test[ i ], applyTransformations, strictCheck ) ) return true; } return false; } var element, result, cacheKey; if ( typeof test == 'string' ) { cacheKey = test + '<' + ( applyTransformations === false ? '0' : '1' ) + ( strictCheck ? '1' : '0' ) + '>'; // Check if result of this check hasn't been already cached. if ( cacheKey in this._.cachedChecks ) return this._.cachedChecks[ cacheKey ]; // Create test element from string. element = mockElementFromString( test ); } else { // Create test element from CKEDITOR.style. element = mockElementFromStyle( test ); } // Make a deep copy. var clone = CKEDITOR.tools.clone( element ), toBeRemoved = [], transformations; // Apply transformations to original element. // Transformations will be applied to clone by the filter function. if ( applyTransformations !== false && ( transformations = this._.transformations[ element.name ] ) ) { for ( i = 0; i < transformations.length; ++i ) applyTransformationsGroup( this, element, transformations[ i ] ); // Transformations could modify styles or classes, so they need to be copied // to attributes object. updateAttributes( element ); } // Filter clone of mocked element. processElement( this, clone, toBeRemoved, { doFilter: true, doTransform: applyTransformations !== false, skipRequired: !strictCheck, skipFinalValidation: !strictCheck } ); // Element has been marked for removal. if ( toBeRemoved.length > 0 ) { result = false; // Compare only left to right, because clone may be only trimmed version of original element. } else if ( !CKEDITOR.tools.objectCompare( element.attributes, clone.attributes, true ) ) { result = false; } else { result = true; } // Cache result of this test - we can build cache only for string tests. if ( typeof test == 'string' ) this._.cachedChecks[ cacheKey ] = result; return result; }, /** * Returns first enter mode allowed by this filter rules. Modes are checked in `p`, `div`, `br` order. * If none of tags is allowed this method will return {@link CKEDITOR#ENTER_BR}. * * @since 4.3 * @param {Number} defaultMode The default mode which will be checked as the first one. * @param {Boolean} [reverse] Whether to check modes in reverse order (used for shift enter mode). * @returns {Number} Allowed enter mode. */ getAllowedEnterMode: ( function() { var tagsToCheck = [ 'p', 'div', 'br' ], enterModes = { p: CKEDITOR.ENTER_P, div: CKEDITOR.ENTER_DIV, br: CKEDITOR.ENTER_BR }; return function( defaultMode, reverse ) { // Clone the array first. var tags = tagsToCheck.slice(), tag; // Check the default mode first. if ( this.check( enterModeTags[ defaultMode ] ) ) return defaultMode; // If not reverse order, reverse array so we can pop() from it. if ( !reverse ) tags = tags.reverse(); while ( ( tag = tags.pop() ) ) { if ( this.check( tag ) ) return enterModes[ tag ]; } return CKEDITOR.ENTER_BR; }; } )(), /** * Destroys the filter instance and removes it from the global {@link CKEDITOR.filter#instances} object. * * @since 4.4.5 */ destroy: function() { delete CKEDITOR.filter.instances[ this.id ]; // Deleting reference to filter instance should be enough, // but since these are big objects it's safe to clean them up too. delete this._; delete this.allowedContent; delete this.disallowedContent; } }; function addAndOptimizeRules( that, newRules, featureName, standardizedRules, optimizedRules ) { var groupName, rule, rulesToOptimize = []; for ( groupName in newRules ) { rule = newRules[ groupName ]; // { 'p h1': true } => { 'p h1': {} }. if ( typeof rule == 'boolean' ) rule = {}; // { 'p h1': func } => { 'p h1': { match: func } }. else if ( typeof rule == 'function' ) rule = { match: rule }; // Clone (shallow) rule, because we'll modify it later. else rule = copy( rule ); // If this is not an unnamed rule ({ '$1' => { ... } }) // move elements list to property. if ( groupName.charAt( 0 ) != '$' ) rule.elements = groupName; if ( featureName ) rule.featureName = featureName.toLowerCase(); standardizeRule( rule ); // Save rule and remember to optimize it. standardizedRules.push( rule ); rulesToOptimize.push( rule ); } optimizeRules( optimizedRules, rulesToOptimize ); } // Apply ACR to an element. // @param rule // @param element // @param status Object containing status of element's filtering. // @param {Boolean} skipRequired If true don't check if element has all required properties. function applyAllowedRule( rule, element, status, skipRequired ) { // This rule doesn't match this element - skip it. if ( rule.match && !rule.match( element ) ) return; // If element doesn't have all required styles/attrs/classes // this rule doesn't match it. if ( !skipRequired && !hasAllRequired( rule, element ) ) return; // If this rule doesn't validate properties only mark element as valid. if ( !rule.propertiesOnly ) status.valid = true; // Apply rule only when all attrs/styles/classes haven't been marked as valid. if ( !status.allAttributes ) status.allAttributes = applyAllowedRuleToHash( rule.attributes, element.attributes, status.validAttributes ); if ( !status.allStyles ) status.allStyles = applyAllowedRuleToHash( rule.styles, element.styles, status.validStyles ); if ( !status.allClasses ) status.allClasses = applyAllowedRuleToArray( rule.classes, element.classes, status.validClasses ); } // Apply itemsRule to items (only classes are kept in array). // Push accepted items to validItems array. // Return true when all items are valid. function applyAllowedRuleToArray( itemsRule, items, validItems ) { if ( !itemsRule ) return false; // True means that all elements of array are accepted (the asterix was used for classes). if ( itemsRule === true ) return true; for ( var i = 0, l = items.length, item; i < l; ++i ) { item = items[ i ]; if ( !validItems[ item ] ) validItems[ item ] = itemsRule( item ); } return false; } function applyAllowedRuleToHash( itemsRule, items, validItems ) { if ( !itemsRule ) return false; if ( itemsRule === true ) return true; for ( var name in items ) { if ( !validItems[ name ] ) validItems[ name ] = itemsRule( name ); } return false; } // Apply DACR rule to an element. function applyDisallowedRule( rule, element, status ) { // This rule doesn't match this element - skip it. if ( rule.match && !rule.match( element ) ) return; // No properties - it's an element only rule so it disallows entire element. // Early return is handled in filterElement. if ( rule.noProperties ) return false; // Apply rule to attributes, styles and classes. Switch hadInvalid* to true if method returned true. status.hadInvalidAttribute = applyDisallowedRuleToHash( rule.attributes, element.attributes ) || status.hadInvalidAttribute; status.hadInvalidStyle = applyDisallowedRuleToHash( rule.styles, element.styles ) || status.hadInvalidStyle; status.hadInvalidClass = applyDisallowedRuleToArray( rule.classes, element.classes ) || status.hadInvalidClass; } // Apply DACR to items (only classes are kept in array). // @returns {Boolean} True if at least one of items was invalid (disallowed). function applyDisallowedRuleToArray( itemsRule, items ) { if ( !itemsRule ) return false; var hadInvalid = false, allDisallowed = itemsRule === true; for ( var i = items.length; i--; ) { if ( allDisallowed || itemsRule( items[ i ] ) ) { items.splice( i, 1 ); hadInvalid = true; } } return hadInvalid; } // Apply DACR to items (styles and attributes). // @returns {Boolean} True if at least one of items was invalid (disallowed). function applyDisallowedRuleToHash( itemsRule, items ) { if ( !itemsRule ) return false; var hadInvalid = false, allDisallowed = itemsRule === true; for ( var name in items ) { if ( allDisallowed || itemsRule( name ) ) { delete items[ name ]; hadInvalid = true; } } return hadInvalid; } function beforeAddingRule( that, newRules, overrideCustom ) { if ( that.disabled ) return false; // Don't override custom user's configuration if not explicitly requested. if ( that.customConfig && !overrideCustom ) return false; if ( !newRules ) return false; // Clear cache, because new rules could change results of checks. that._.cachedChecks = {}; return true; } // Convert CKEDITOR.style to filter's rule. function convertStyleToRules( style ) { var styleDef = style.getDefinition(), rules = {}, rule, attrs = styleDef.attributes; rules[ styleDef.element ] = rule = { styles: styleDef.styles, requiredStyles: styleDef.styles && CKEDITOR.tools.objectKeys( styleDef.styles ) }; if ( attrs ) { attrs = copy( attrs ); rule.classes = attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : null; rule.requiredClasses = rule.classes; delete attrs[ 'class' ]; rule.attributes = attrs; rule.requiredAttributes = attrs && CKEDITOR.tools.objectKeys( attrs ); } return rules; } // Convert all validator formats (string, array, object, boolean) to hash or boolean: // * true is returned for '*'/true validator, // * false is returned for empty validator (no validator at all (false/null) or e.g. empty array), // * object is returned in other cases. function convertValidatorToHash( validator, delimiter ) { if ( !validator ) return false; if ( validator === true ) return validator; if ( typeof validator == 'string' ) { validator = trim( validator ); if ( validator == '*' ) return true; else return CKEDITOR.tools.convertArrayToObject( validator.split( delimiter ) ); } else if ( CKEDITOR.tools.isArray( validator ) ) { if ( validator.length ) return CKEDITOR.tools.convertArrayToObject( validator ); else return false; } // If object. else { var obj = {}, len = 0; for ( var i in validator ) { obj[ i ] = validator[ i ]; len++; } return len ? obj : false; } } function executeElementCallbacks( element, callbacks ) { for ( var i = 0, l = callbacks.length, retVal; i < l; ++i ) { if ( ( retVal = callbacks[ i ]( element ) ) ) return retVal; } } // Extract required properties from "required" validator and "all" properties. // Remove exclamation marks from "all" properties. // // E.g.: // requiredClasses = { cl1: true } // (all) classes = { cl1: true, cl2: true, '!cl3': true } // // result: // returned = { cl1: true, cl3: true } // all = { cl1: true, cl2: true, cl3: true } // // This function returns false if nothing is required. function extractRequired( required, all ) { var unbang = [], empty = true, i; if ( required ) empty = false; else required = {}; for ( i in all ) { if ( i.charAt( 0 ) == '!' ) { i = i.slice( 1 ); unbang.push( i ); required[ i ] = true; empty = false; } } while ( ( i = unbang.pop() ) ) { all[ i ] = all[ '!' + i ]; delete all[ '!' + i ]; } return empty ? false : required; } // Does the actual filtering by appling allowed content rules // to the element. // // @param {CKEDITOR.filter} that The context. // @param {CKEDITOR.htmlParser.element} element // @param {Object} opts The same as in processElement. function filterElement( that, element, opts ) { var name = element.name, privObj = that._, allowedRules = privObj.allowedRules.elements[ name ], genericAllowedRules = privObj.allowedRules.generic, disallowedRules = privObj.disallowedRules.elements[ name ], genericDisallowedRules = privObj.disallowedRules.generic, skipRequired = opts.skipRequired, status = { // Whether any of rules accepted element. // If not - it will be stripped. valid: false, // Objects containing accepted attributes, classes and styles. validAttributes: {}, validClasses: {}, validStyles: {}, // Whether all are valid. // If we know that all element's attrs/classes/styles are valid // we can skip their validation, to improve performance. allAttributes: false, allClasses: false, allStyles: false, // Whether element had (before applying DACRs) at least one invalid attribute/class/style. hadInvalidAttribute: false, hadInvalidClass: false, hadInvalidStyle: false }, i, l; // Early return - if there are no rules for this element (specific or generic), remove it. if ( !allowedRules && !genericAllowedRules ) return null; // Could not be done yet if there were no transformations and if this // is real (not mocked) object. populateProperties( element ); // Note - this step modifies element's styles, classes and attributes. if ( disallowedRules ) { for ( i = 0, l = disallowedRules.length; i < l; ++i ) { // Apply rule and make an early return if false is returned what means // that element is completely disallowed. if ( applyDisallowedRule( disallowedRules[ i ], element, status ) === false ) return null; } } // Note - this step modifies element's styles, classes and attributes. if ( genericDisallowedRules ) { for ( i = 0, l = genericDisallowedRules.length; i < l; ++i ) applyDisallowedRule( genericDisallowedRules[ i ], element, status ); } if ( allowedRules ) { for ( i = 0, l = allowedRules.length; i < l; ++i ) applyAllowedRule( allowedRules[ i ], element, status, skipRequired ); } if ( genericAllowedRules ) { for ( i = 0, l = genericAllowedRules.length; i < l; ++i ) applyAllowedRule( genericAllowedRules[ i ], element, status, skipRequired ); } return status; } // Check whether element has all properties (styles,classes,attrs) required by a rule. function hasAllRequired( rule, element ) { if ( rule.nothingRequired ) return true; var i, req, reqs, existing; if ( ( reqs = rule.requiredClasses ) ) { existing = element.classes; for ( i = 0; i < reqs.length; ++i ) { req = reqs[ i ]; if ( typeof req == 'string' ) { if ( CKEDITOR.tools.indexOf( existing, req ) == -1 ) return false; } // This means regexp. else { if ( !CKEDITOR.tools.checkIfAnyArrayItemMatches( existing, req ) ) return false; } } } return hasAllRequiredInHash( element.styles, rule.requiredStyles ) && hasAllRequiredInHash( element.attributes, rule.requiredAttributes ); } // Check whether all items in required (array) exist in existing (object). function hasAllRequiredInHash( existing, required ) { if ( !required ) return true; for ( var i = 0, req; i < required.length; ++i ) { req = required[ i ]; if ( typeof req == 'string' ) { if ( !( req in existing ) ) return false; } // This means regexp. else { if ( !CKEDITOR.tools.checkIfAnyObjectPropertyMatches( existing, req ) ) return false; } } return true; } // Create pseudo element that will be passed through filter // to check if tested string is allowed. function mockElementFromString( str ) { var element = parseRulesString( str ).$1, styles = element.styles, classes = element.classes; element.name = element.elements; element.classes = classes = ( classes ? classes.split( /\s*,\s*/ ) : [] ); element.styles = mockHash( styles ); element.attributes = mockHash( element.attributes ); element.children = []; if ( classes.length ) element.attributes[ 'class' ] = classes.join( ' ' ); if ( styles ) element.attributes.style = CKEDITOR.tools.writeCssText( element.styles ); return element; } // Create pseudo element that will be passed through filter // to check if tested style is allowed. function mockElementFromStyle( style ) { var styleDef = style.getDefinition(), styles = styleDef.styles, attrs = styleDef.attributes || {}; if ( styles && !CKEDITOR.tools.isEmpty( styles ) ) { styles = copy( styles ); attrs.style = CKEDITOR.tools.writeCssText( styles, true ); } else { styles = {}; } return { name: styleDef.element, attributes: attrs, classes: attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : [], styles: styles, children: [] }; } // Mock hash based on string. // 'a,b,c' => { a: 'cke-test', b: 'cke-test', c: 'cke-test' } // Used to mock styles and attributes objects. function mockHash( str ) { // It may be a null or empty string. if ( !str ) return {}; var keys = str.split( /\s*,\s*/ ).sort(), obj = {}; while ( keys.length ) obj[ keys.shift() ] = TEST_VALUE; return obj; } // Extract properties names from the object // and replace those containing wildcards with regexps. // Note: there's a room for performance improvement. Array of mixed types // breaks JIT-compiler optiomization what may invalidate compilation of pretty a lot of code. // // @returns An array of strings and regexps. function optimizeRequiredProperties( requiredProperties ) { var arr = []; for ( var propertyName in requiredProperties ) { if ( propertyName.indexOf( '*' ) > -1 ) arr.push( new RegExp( '^' + propertyName.replace( /\*/g, '.*' ) + '$' ) ); else arr.push( propertyName ); } return arr; } var validators = { styles: 1, attributes: 1, classes: 1 }, validatorsRequired = { styles: 'requiredStyles', attributes: 'requiredAttributes', classes: 'requiredClasses' }; // Optimize a rule by replacing validators with functions // and rewriting requiredXXX validators to arrays. function optimizeRule( rule ) { var validatorName, requiredProperties, i; for ( validatorName in validators ) rule[ validatorName ] = validatorFunction( rule[ validatorName ] ); var nothingRequired = true; for ( i in validatorsRequired ) { validatorName = validatorsRequired[ i ]; requiredProperties = optimizeRequiredProperties( rule[ validatorName ] ); // Don't set anything if there are no required properties. This will allow to // save some memory by GCing all empty arrays (requiredProperties). if ( requiredProperties.length ) { rule[ validatorName ] = requiredProperties; nothingRequired = false; } } rule.nothingRequired = nothingRequired; rule.noProperties = !( rule.attributes || rule.classes || rule.styles ); } // Add optimized version of rule to optimizedRules object. function optimizeRules( optimizedRules, rules ) { var elementsRules = optimizedRules.elements, genericRules = optimizedRules.generic, i, l, rule, element, priority; for ( i = 0, l = rules.length; i < l; ++i ) { // Shallow copy. Do not modify original rule. rule = copy( rules[ i ] ); priority = rule.classes === true || rule.styles === true || rule.attributes === true; optimizeRule( rule ); // E.g. "*(xxx)[xxx]" - it's a generic rule that // validates properties only. // Or '$1': { match: function() {...} } if ( rule.elements === true || rule.elements === null ) { // Add priority rules at the beginning. genericRules[ priority ? 'unshift' : 'push' ]( rule ); } // If elements list was explicitly defined, // add this rule for every defined element. else { // We don't need elements validator for this kind of rule. var elements = rule.elements; delete rule.elements; for ( element in elements ) { if ( !elementsRules[ element ] ) elementsRules[ element ] = [ rule ]; else elementsRules[ element ][ priority ? 'unshift' : 'push' ]( rule ); } } } } // < elements >< styles, attributes and classes >< separator > var rulePattern = /^([a-z0-9\-*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i, groupsPatterns = { styles: /{([^}]+)}/, attrs: /\[([^\]]+)\]/, classes: /\(([^\)]+)\)/ }; function parseRulesString( input ) { var match, props, styles, attrs, classes, rules = {}, groupNum = 1; input = trim( input ); while ( ( match = input.match( rulePattern ) ) ) { if ( ( props = match[ 2 ] ) ) { styles = parseProperties( props, 'styles' ); attrs = parseProperties( props, 'attrs' ); classes = parseProperties( props, 'classes' ); } else { styles = attrs = classes = null; } // Add as an unnamed rule, because there can be two rules // for one elements set defined in string format. rules[ '$' + groupNum++ ] = { elements: match[ 1 ], classes: classes, styles: styles, attributes: attrs }; // Move to the next group. input = input.slice( match[ 0 ].length ); } return rules; } // Extract specified properties group (styles, attrs, classes) from // what stands after the elements list in string format of allowedContent. function parseProperties( properties, groupName ) { var group = properties.match( groupsPatterns[ groupName ] ); return group ? trim( group[ 1 ] ) : null; } function populateProperties( element ) { // Backup styles and classes, because they may be removed by DACRs. // We'll need them in updateElement(). var styles = element.styleBackup = element.attributes.style, classes = element.classBackup = element.attributes[ 'class' ]; // Parse classes and styles if that hasn't been done before. if ( !element.styles ) element.styles = CKEDITOR.tools.parseCssText( styles || '', 1 ); if ( !element.classes ) element.classes = classes ? classes.split( /\s+/ ) : []; } // Filter element protected with a comment. // Returns true if protected content is ok, false otherwise. function processProtectedElement( that, comment, protectedRegexs, filterOpts ) { var source = decodeURIComponent( comment.value.replace( /^\{cke_protected\}/, '' ) ), protectedFrag, toBeRemoved = [], node, i, match; // Protected element's and protected source's comments look exactly the same. // Check if what we have isn't a protected source instead of protected script/noscript. if ( protectedRegexs ) { for ( i = 0; i < protectedRegexs.length; ++i ) { if ( ( match = source.match( protectedRegexs[ i ] ) ) && match[ 0 ].length == source.length // Check whether this pattern matches entire source // to avoid '<script>alert("<? 1 ?>")</script>' matching // the PHP's protectedSource regexp. ) return true; } } protectedFrag = CKEDITOR.htmlParser.fragment.fromHtml( source ); if ( protectedFrag.children.length == 1 && ( node = protectedFrag.children[ 0 ] ).type == CKEDITOR.NODE_ELEMENT ) processElement( that, node, toBeRemoved, filterOpts ); // If protected element has been marked to be removed, return 'false' - comment was rejected. return !toBeRemoved.length; } var unprotectElementsNamesRegexp = /^cke:(object|embed|param)$/, protectElementsNamesRegexp = /^(object|embed|param)$/; // The actual function which filters, transforms and does other funny things with an element. // // @param {CKEDITOR.filter} that Context. // @param {CKEDITOR.htmlParser.element} element The element to be processed. // @param {Array} toBeRemoved Array into which elements rejected by the filter will be pushed. // @param {Boolean} [opts.doFilter] Whether element should be filtered. // @param {Boolean} [opts.doTransform] Whether transformations should be applied. // @param {Boolean} [opts.doCallbacks] Whether to execute element callbacks. // @param {Boolean} [opts.toHtml] Set to true if filter used together with htmlDP#toHtml // @param {Boolean} [opts.skipRequired] Whether element's required properties shouldn't be verified. // @param {Boolean} [opts.skipFinalValidation] Whether to not perform final element validation (a,img). // @returns {Number} Possible flags: // * FILTER_ELEMENT_MODIFIED, // * FILTER_SKIP_TREE. function processElement( that, element, toBeRemoved, opts ) { var status, retVal = 0, callbacksRetVal; // Unprotect elements names previously protected by htmlDataProcessor // (see protectElementNames and protectSelfClosingElements functions). // Note: body, title, etc. are not protected by htmlDataP (or are protected and then unprotected). if ( opts.toHtml ) element.name = element.name.replace( unprotectElementsNamesRegexp, '$1' ); // Execute element callbacks and return if one of them returned any value. if ( opts.doCallbacks && that.elementCallbacks ) { // For now we only support here FILTER_SKIP_TREE, so we can early return if retVal is truly value. if ( ( callbacksRetVal = executeElementCallbacks( element, that.elementCallbacks ) ) ) return callbacksRetVal; } // If transformations are set apply all groups. if ( opts.doTransform ) transformElement( that, element ); if ( opts.doFilter ) { // Apply all filters. status = filterElement( that, element, opts ); // Handle early return from filterElement. if ( !status ) { toBeRemoved.push( element ); return FILTER_ELEMENT_MODIFIED; } // Finally, if after running all filter rules it still hasn't been allowed - remove it. if ( !status.valid ) { toBeRemoved.push( element ); return FILTER_ELEMENT_MODIFIED; } // Update element's attributes based on status of filtering. if ( updateElement( element, status ) ) retVal = FILTER_ELEMENT_MODIFIED; if ( !opts.skipFinalValidation && !validateElement( element ) ) { toBeRemoved.push( element ); return FILTER_ELEMENT_MODIFIED; } } // Protect previously unprotected elements. if ( opts.toHtml ) element.name = element.name.replace( protectElementsNamesRegexp, 'cke:$1' ); return retVal; } // Returns a regexp object which can be used to test if a property // matches one of wildcard validators. function regexifyPropertiesWithWildcards( validators ) { var patterns = [], i; for ( i in validators ) { if ( i.indexOf( '*' ) > -1 ) patterns.push( i.replace( /\*/g, '.*' ) ); } if ( patterns.length ) return new RegExp( '^(?:' + patterns.join( '|' ) + ')$' ); else return null; } // Standardize a rule by converting all validators to hashes. function standardizeRule( rule ) { rule.elements = convertValidatorToHash( rule.elements, /\s+/ ) || null; rule.propertiesOnly = rule.propertiesOnly || ( rule.elements === true ); var delim = /\s*,\s*/, i; for ( i in validators ) { rule[ i ] = convertValidatorToHash( rule[ i ], delim ) || null; rule[ validatorsRequired[ i ] ] = extractRequired( convertValidatorToHash( rule[ validatorsRequired[ i ] ], delim ), rule[ i ] ) || null; } rule.match = rule.match || null; } // Does the element transformation by applying registered // transformation rules. function transformElement( that, element ) { var transformations = that._.transformations[ element.name ], i; if ( !transformations ) return; populateProperties( element ); for ( i = 0; i < transformations.length; ++i ) applyTransformationsGroup( that, element, transformations[ i ] ); // Do not count on updateElement() which is called in processElement, because it: // * may not be called, // * may skip some properties when all are marked as valid. updateAttributes( element ); } // Copy element's styles and classes back to attributes array. function updateAttributes( element ) { var attrs = element.attributes, styles; // Will be recreated later if any of styles/classes exists. delete attrs.style; delete attrs[ 'class' ]; if ( ( styles = CKEDITOR.tools.writeCssText( element.styles, true ) ) ) attrs.style = styles; if ( element.classes.length ) attrs[ 'class' ] = element.classes.sort().join( ' ' ); } // Update element object based on status of filtering. // @returns Whether element was modified. function updateElement( element, status ) { var validAttrs = status.validAttributes, validStyles = status.validStyles, validClasses = status.validClasses, attrs = element.attributes, styles = element.styles, classes = element.classes, origClasses = element.classBackup, origStyles = element.styleBackup, name, origName, i, stylesArr = [], classesArr = [], internalAttr = /^data-cke-/, isModified = false; // Will be recreated later if any of styles/classes were passed. delete attrs.style; delete attrs[ 'class' ]; // Clean up. delete element.classBackup; delete element.styleBackup; if ( !status.allAttributes ) { for ( name in attrs ) { // If not valid and not internal attribute delete it. if ( !validAttrs[ name ] ) { // Allow all internal attibutes... if ( internalAttr.test( name ) ) { // ... unless this is a saved attribute and the original one isn't allowed. if ( name != ( origName = name.replace( /^data-cke-saved-/, '' ) ) && !validAttrs[ origName ] ) { delete attrs[ name ]; isModified = true; } } else { delete attrs[ name ]; isModified = true; } } } } if ( !status.allStyles || status.hadInvalidStyle ) { for ( name in styles ) { // We check status.allStyles because when there was a '*' ACR and some // DACR we have now both properties true - status.allStyles and status.hadInvalidStyle. // However unlike in the case when we only have '*' ACR, in which we can just copy original // styles, in this case we must copy only those styles which were not removed by DACRs. if ( status.allStyles || validStyles[ name ] ) stylesArr.push( name + ':' + styles[ name ] ); else isModified = true; } if ( stylesArr.length ) attrs.style = stylesArr.sort().join( '; ' ); } else if ( origStyles ) { attrs.style = origStyles; } if ( !status.allClasses || status.hadInvalidClass ) { for ( i = 0; i < classes.length; ++i ) { // See comment for styles. if ( status.allClasses || validClasses[ classes[ i ] ] ) classesArr.push( classes[ i ] ); } if ( classesArr.length ) attrs[ 'class' ] = classesArr.sort().join( ' ' ); if ( origClasses && classesArr.length < origClasses.split( /\s+/ ).length ) isModified = true; } else if ( origClasses ) { attrs[ 'class' ] = origClasses; } return isModified; } function validateElement( element ) { switch ( element.name ) { case 'a': // Code borrowed from htmlDataProcessor, so ACF does the same clean up. if ( !( element.children.length || element.attributes.name || element.attributes.id ) ) return false; break; case 'img': if ( !element.attributes.src ) return false; break; } return true; } function validatorFunction( validator ) { if ( !validator ) return false; if ( validator === true ) return true; // Note: We don't need to remove properties with wildcards from the validator object. // E.g. data-* is actually an edge case of /^data-.*$/, so when it's accepted // by `value in validator` it's ok. var regexp = regexifyPropertiesWithWildcards( validator ); return function( value ) { return value in validator || ( regexp && value.match( regexp ) ); }; } // // REMOVE ELEMENT --------------------------------------------------------- // // Check whether all children will be valid in new context. // Note: it doesn't verify if text node is valid, because // new parent should accept them. function checkChildren( children, newParentName ) { var allowed = DTD[ newParentName ]; for ( var i = 0, l = children.length, child; i < l; ++i ) { child = children[ i ]; if ( child.type == CKEDITOR.NODE_ELEMENT && !allowed[ child.name ] ) return false; } return true; } function createBr() { return new CKEDITOR.htmlParser.element( 'br' ); } // Whether this is an inline element or text. function inlineNode( node ) { return node.type == CKEDITOR.NODE_TEXT || node.type == CKEDITOR.NODE_ELEMENT && DTD.$inline[ node.name ]; } function isBrOrBlock( node ) { return node.type == CKEDITOR.NODE_ELEMENT && ( node.name == 'br' || DTD.$block[ node.name ] ); } // Try to remove element in the best possible way. // // @param {Array} toBeChecked After executing this function // this array will contain elements that should be checked // because they were marked as potentially: // * in wrong context (e.g. li in body), // * empty elements from $removeEmpty, // * incorrect img/a/other element validated by validateElement(). function removeElement( element, enterTag, toBeChecked ) { var name = element.name; if ( DTD.$empty[ name ] || !element.children.length ) { // Special case - hr in br mode should be replaced with br, not removed. if ( name == 'hr' && enterTag == 'br' ) element.replaceWith( createBr() ); else { // Parent might become an empty inline specified in $removeEmpty or empty a[href]. if ( element.parent ) toBeChecked.push( { check: 'it', el: element.parent } ); element.remove(); } } else if ( DTD.$block[ name ] || name == 'tr' ) { if ( enterTag == 'br' ) stripBlockBr( element, toBeChecked ); else stripBlock( element, enterTag, toBeChecked ); } // Special case - elements that may contain CDATA should be removed completely. else if ( name in { style: 1, script: 1 } ) element.remove(); // The rest of inline elements. May also be the last resort // for some special elements. else { // Parent might become an empty inline specified in $removeEmpty or empty a[href]. if ( element.parent ) toBeChecked.push( { check: 'it', el: element.parent } ); element.replaceWithChildren(); } } // Strip element block, but leave its content. // Works in 'div' and 'p' enter modes. function stripBlock( element, enterTag, toBeChecked ) { var children = element.children; // First, check if element's children may be wrapped with <p/div>. // Ignore that <p/div> may not be allowed in element.parent. // This will be fixed when removing parent or by toBeChecked rule. if ( checkChildren( children, enterTag ) ) { element.name = enterTag; element.attributes = {}; // Check if this p/div was put in correct context. // If not - strip parent. toBeChecked.push( { check: 'parent-down', el: element } ); return; } var parent = element.parent, shouldAutoP = parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT || parent.name == 'body', i, child, p, parentDtd; for ( i = children.length; i > 0; ) { child = children[ --i ]; // If parent requires auto paragraphing and child is inline node, // insert this child into newly created paragraph. if ( shouldAutoP && inlineNode( child ) ) { if ( !p ) { p = new CKEDITOR.htmlParser.element( enterTag ); p.insertAfter( element ); // Check if this p/div was put in correct context. // If not - strip parent. toBeChecked.push( { check: 'parent-down', el: p } ); } p.add( child, 0 ); } // Child which doesn't need to be auto paragraphed. else { p = null; parentDtd = DTD[ parent.name ] || DTD.span; child.insertAfter( element ); // If inserted into invalid context, mark it and check // after removing all elements. if ( parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && child.type == CKEDITOR.NODE_ELEMENT && !parentDtd[ child.name ] ) toBeChecked.push( { check: 'el-up', el: child } ); } } // All children have been moved to element's parent, so remove it. element.remove(); } // Prepend/append block with <br> if isn't // already prepended/appended with <br> or block and // isn't first/last child of its parent. // Then replace element with its children. // <p>a</p><p>b</p> => <p>a</p><br>b => a<br>b function stripBlockBr( element ) { var br; if ( element.previous && !isBrOrBlock( element.previous ) ) { br = createBr(); br.insertBefore( element ); } if ( element.next && !isBrOrBlock( element.next ) ) { br = createBr(); br.insertAfter( element ); } element.replaceWithChildren(); } // // TRANSFORMATIONS -------------------------------------------------------- // var transformationsTools; // Apply given transformations group to the element. function applyTransformationsGroup( filter, element, group ) { var i, rule; for ( i = 0; i < group.length; ++i ) { rule = group[ i ]; // Test with #check or #left only if it's set. // Do not apply transformations because that creates infinite loop. if ( ( !rule.check || filter.check( rule.check, false ) ) && ( !rule.left || rule.left( element ) ) ) { rule.right( element, transformationsTools ); return; // Only first matching rule in a group is executed. } } } // Check whether element matches CKEDITOR.style. // The element can be a "superset" of style, // e.g. it may have more classes, but need to have // at least those defined in style. function elementMatchesStyle( element, style ) { var def = style.getDefinition(), defAttrs = def.attributes, defStyles = def.styles, attrName, styleName, classes, classPattern, cl; if ( element.name != def.element ) return false; for ( attrName in defAttrs ) { if ( attrName == 'class' ) { classes = defAttrs[ attrName ].split( /\s+/ ); classPattern = element.classes.join( '|' ); while ( ( cl = classes.pop() ) ) { if ( classPattern.indexOf( cl ) == -1 ) return false; } } else { if ( element.attributes[ attrName ] != defAttrs[ attrName ] ) return false; } } for ( styleName in defStyles ) { if ( element.styles[ styleName ] != defStyles[ styleName ] ) return false; } return true; } // Return transformation group for content form. // One content form makes one transformation rule in one group. function getContentFormTransformationGroup( form, preferredForm ) { var element, left; if ( typeof form == 'string' ) element = form; else if ( form instanceof CKEDITOR.style ) left = form; else { element = form[ 0 ]; left = form[ 1 ]; } return [ { element: element, left: left, right: function( el, tools ) { tools.transform( el, preferredForm ); } } ]; } // Obtain element's name from transformation rule. // It will be defined by #element, or #check or #left (styleDef.element). function getElementNameForTransformation( rule, check ) { if ( rule.element ) return rule.element; if ( check ) return check.match( /^([a-z0-9]+)/i )[ 0 ]; return rule.left.getDefinition().element; } function getMatchStyleFn( style ) { return function( el ) { return elementMatchesStyle( el, style ); }; } function getTransformationFn( toolName ) { return function( el, tools ) { tools[ toolName ]( el ); }; } function optimizeTransformationsGroup( rules ) { var groupName, i, rule, check, left, right, optimizedRules = []; for ( i = 0; i < rules.length; ++i ) { rule = rules[ i ]; if ( typeof rule == 'string' ) { rule = rule.split( /\s*:\s*/ ); check = rule[ 0 ]; left = null; right = rule[ 1 ]; } else { check = rule.check; left = rule.left; right = rule.right; } // Extract element name. if ( !groupName ) groupName = getElementNameForTransformation( rule, check ); if ( left instanceof CKEDITOR.style ) left = getMatchStyleFn( left ); optimizedRules.push( { // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. check: check == groupName ? null : check, left: left, // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'. right: typeof right == 'string' ? getTransformationFn( right ) : right } ); } return { name: groupName, rules: optimizedRules }; } /** * Singleton containing tools useful for transformation rules. * * @class CKEDITOR.filter.transformationsTools * @singleton */ transformationsTools = CKEDITOR.filter.transformationsTools = { /** * Converts `width` and `height` attributes to styles. * * @param {CKEDITOR.htmlParser.element} element */ sizeToStyle: function( element ) { this.lengthToStyle( element, 'width' ); this.lengthToStyle( element, 'height' ); }, /** * Converts `width` and `height` styles to attributes. * * @param {CKEDITOR.htmlParser.element} element */ sizeToAttribute: function( element ) { this.lengthToAttribute( element, 'width' ); this.lengthToAttribute( element, 'height' ); }, /** * Converts length in the `attrName` attribute to a valid CSS length (like `width` or `height`). * * @param {CKEDITOR.htmlParser.element} element * @param {String} attrName Name of the attribute that will be converted. * @param {String} [styleName=attrName] Name of the style into which the attribute will be converted. */ lengthToStyle: function( element, attrName, styleName ) { styleName = styleName || attrName; if ( !( styleName in element.styles ) ) { var value = element.attributes[ attrName ]; if ( value ) { if ( ( /^\d+$/ ).test( value ) ) value += 'px'; element.styles[ styleName ] = value; } } delete element.attributes[ attrName ]; }, /** * Converts length in the `styleName` style to a valid length attribute (like `width` or `height`). * * @param {CKEDITOR.htmlParser.element} element * @param {String} styleName The name of the style that will be converted. * @param {String} [attrName=styleName] The name of the attribute into which the style will be converted. */ lengthToAttribute: function( element, styleName, attrName ) { attrName = attrName || styleName; if ( !( attrName in element.attributes ) ) { var value = element.styles[ styleName ], match = value && value.match( /^(\d+)(?:\.\d*)?px$/ ); if ( match ) element.attributes[ attrName ] = match[ 1 ]; // Pass the TEST_VALUE used by filter#check when mocking element. else if ( value == TEST_VALUE ) element.attributes[ attrName ] = TEST_VALUE; } delete element.styles[ styleName ]; }, /** * Converts the `align` attribute to the `float` style if not set. The attribute * is always removed. * * @param {CKEDITOR.htmlParser.element} element */ alignmentToStyle: function( element ) { if ( !( 'float' in element.styles ) ) { var value = element.attributes.align; if ( value == 'left' || value == 'right' ) element.styles[ 'float' ] = value; // Uh... GCC doesn't like the 'float' prop name. } delete element.attributes.align; }, /** * Converts the `float` style to the `align` attribute if not set. * The style is always removed. * * @param {CKEDITOR.htmlParser.element} element */ alignmentToAttribute: function( element ) { if ( !( 'align' in element.attributes ) ) { var value = element.styles[ 'float' ]; if ( value == 'left' || value == 'right' ) element.attributes.align = value; } delete element.styles[ 'float' ]; // Uh... GCC doesn't like the 'float' prop name. }, /** * Converts the shorthand form of the `border` style to seperate styles. * * @param {CKEDITOR.htmlParser.element} element */ splitBorderShorthand: function( element ) { if ( !element.styles.border ) { return; } var widths = element.styles.border.match( /([\.\d]+\w+)/g ) || [ '0px' ]; switch ( widths.length ) { case 1: element.styles[ 'border-width' ] = widths[0]; break; case 2: mapStyles( [ 0, 1, 0, 1 ] ); break; case 3: mapStyles( [ 0, 1, 2, 1 ] ); break; case 4: mapStyles( [ 0, 1, 2, 3 ] ); break; } element.styles[ 'border-style' ] = element.styles[ 'border-style' ] || ( element.styles.border.match( /(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset|initial|inherit)/ ) || [] )[ 0 ]; if ( !element.styles[ 'border-style' ] ) delete element.styles[ 'border-style' ]; delete element.styles.border; function mapStyles( map ) { element.styles['border-top-width'] = widths[ map[0] ]; element.styles['border-right-width'] = widths[ map[1] ]; element.styles['border-bottom-width'] = widths[ map[2] ]; element.styles['border-left-width'] = widths[ map[3] ]; } }, listTypeToStyle: function( element ) { if ( element.attributes.type ) { switch ( element.attributes.type ) { case 'a': element.styles[ 'list-style-type' ] = 'lower-alpha'; break; case 'A': element.styles[ 'list-style-type' ] = 'upper-alpha'; break; case 'i': element.styles[ 'list-style-type' ] = 'lower-roman'; break; case 'I': element.styles[ 'list-style-type' ] = 'upper-roman'; break; case '1': element.styles[ 'list-style-type' ] = 'decimal'; break; default: element.styles[ 'list-style-type' ] = element.attributes.type; } } }, /** * Converts the shorthand form of the `margin` style to seperate styles. * * @param {CKEDITOR.htmlParser.element} element */ splitMarginShorthand: function( element ) { if ( !element.styles.margin ) { return; } var widths = element.styles.margin.match( /(\-?[\.\d]+\w+)/g ) || [ '0px' ]; switch ( widths.length ) { case 1: element.styles.margin = widths[0]; break; case 2: mapStyles( [ 0, 1, 0, 1 ] ); break; case 3: mapStyles( [ 0, 1, 2, 1 ] ); break; case 4: mapStyles( [ 0, 1, 2, 3 ] ); break; } delete element.styles.margin; function mapStyles( map ) { element.styles['margin-top'] = widths[ map[0] ]; element.styles['margin-right'] = widths[ map[1] ]; element.styles['margin-bottom'] = widths[ map[2] ]; element.styles['margin-left'] = widths[ map[3] ]; } }, /** * Checks whether an element matches a given {@link CKEDITOR.style}. * The element can be a "superset" of a style, e.g. it may have * more classes, but needs to have at least those defined in the style. * * @param {CKEDITOR.htmlParser.element} element * @param {CKEDITOR.style} style */ matchesStyle: elementMatchesStyle, /** * Transforms an element to a given form. * * Form may be a: * * * {@link CKEDITOR.style}, * * string &ndash; the new name of the element. * * @param {CKEDITOR.htmlParser.element} el * @param {CKEDITOR.style/String} form */ transform: function( el, form ) { if ( typeof form == 'string' ) el.name = form; // Form is an instance of CKEDITOR.style. else { var def = form.getDefinition(), defStyles = def.styles, defAttrs = def.attributes, attrName, styleName, existingClassesPattern, defClasses, cl; el.name = def.element; for ( attrName in defAttrs ) { if ( attrName == 'class' ) { existingClassesPattern = el.classes.join( '|' ); defClasses = defAttrs[ attrName ].split( /\s+/ ); while ( ( cl = defClasses.pop() ) ) { if ( existingClassesPattern.indexOf( cl ) == -1 ) el.classes.push( cl ); } } else { el.attributes[ attrName ] = defAttrs[ attrName ]; } } for ( styleName in defStyles ) { el.styles[ styleName ] = defStyles[ styleName ]; } } } }; } )(); /** * Allowed content rules. This setting is used when * instantiating {@link CKEDITOR.editor#filter}. * * The following values are accepted: * * * {@link CKEDITOR.filter.allowedContentRules} &ndash; defined rules will be added * to the {@link CKEDITOR.editor#filter}. * * `true` &ndash; will disable the filter (data will not be filtered, * all features will be activated). * * default &ndash; the filter will be configured by loaded features * (toolbar items, commands, etc.). * * In all cases filter configuration may be extended by * {@link CKEDITOR.config#extraAllowedContent}. This option may be especially * useful when you want to use the default `allowedContent` value * along with some additional rules. * * CKEDITOR.replace( 'textarea_id', { * allowedContent: 'p b i; a[!href]', * on: { * instanceReady: function( evt ) { * var editor = evt.editor; * * editor.filter.check( 'h1' ); // -> false * editor.setData( '<h1><i>Foo</i></h1><p class="left"><span>Bar</span> <a href="http://foo.bar">foo</a></p>' ); * // Editor contents will be: * '<p><i>Foo</i></p><p>Bar <a href="http://foo.bar">foo</a></p>' * } * } * } ); * * It is also possible to disallow some already allowed content. It is especially * useful when you want to "trim down" the content allowed by default by * editor features. To do that, use the {@link #disallowedContent} option. * * Read more in the [documentation](#!/guide/dev_acf) * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). * * @since 4.1 * @cfg {CKEDITOR.filter.allowedContentRules/Boolean} [allowedContent=null] * @member CKEDITOR.config */ /** * This option makes it possible to set additional allowed * content rules for {@link CKEDITOR.editor#filter}. * * It is especially useful in combination with the default * {@link CKEDITOR.config#allowedContent} value: * * CKEDITOR.replace( 'textarea_id', { * plugins: 'wysiwygarea,toolbar,format', * extraAllowedContent: 'b i', * on: { * instanceReady: function( evt ) { * var editor = evt.editor; * * editor.filter.check( 'h1' ); // -> true (thanks to Format combo) * editor.filter.check( 'b' ); // -> true (thanks to extraAllowedContent) * editor.setData( '<h1><i>Foo</i></h1><p class="left"><b>Bar</b> <a href="http://foo.bar">foo</a></p>' ); * // Editor contents will be: * '<h1><i>Foo</i></h1><p><b>Bar</b> foo</p>' * } * } * } ); * * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-and-allow-additional-tags%2Fproperties) * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). * See also {@link CKEDITOR.config#allowedContent} for more details. * * @since 4.1 * @cfg {Object/String} extraAllowedContent * @member CKEDITOR.config */ /** * Disallowed content rules. They have precedence over {@link #allowedContent allowed content rules}. * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). * * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-but-disallow-certain-tags%2Fproperties) * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). * See also {@link CKEDITOR.config#allowedContent} and {@link CKEDITOR.config#extraAllowedContent}. * * @since 4.4 * @cfg {CKEDITOR.filter.disallowedContentRules} disallowedContent * @member CKEDITOR.config */ /** * This event is fired when {@link CKEDITOR.filter} has stripped some * content from the data that was loaded (e.g. by {@link CKEDITOR.editor#method-setData} * method or in the source mode) or inserted (e.g. when pasting or using the * {@link CKEDITOR.editor#method-insertHtml} method). * * This event is useful when testing whether the {@link CKEDITOR.config#allowedContent} * setting is sufficient and correct for a system that is migrating to CKEditor 4.1 * (where the [Advanced Content Filter](#!/guide/dev_advanced_content_filter) was introduced). * * @since 4.1 * @event dataFiltered * @member CKEDITOR.editor * @param {CKEDITOR.editor} editor This editor instance. */ /** * Virtual class which is the [Allowed Content Rules](#!/guide/dev_allowed_content_rules) formats type. * * Possible formats are: * * * the [string format](#!/guide/dev_allowed_content_rules-section-2), * * the [object format](#!/guide/dev_allowed_content_rules-section-3), * * a {@link CKEDITOR.style} instance &ndash; used mainly for integrating plugins with Advanced Content Filter, * * an array of the above formats. * * @since 4.1 * @class CKEDITOR.filter.allowedContentRules * @abstract */ /** * Virtual class representing the {@link CKEDITOR.filter#disallow} argument and a type of * the {@link CKEDITOR.config#disallowedContent} option. * * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. * Only the string format and object format are accepted. Required properties * are not allowed in this format. * * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). * * @since 4.4 * @class CKEDITOR.filter.disallowedContentRules * @abstract */ /** * Virtual class representing {@link CKEDITOR.filter#check} argument. * * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. * It may contain only one element and its styles, classes, and attributes. Only the * string format and a {@link CKEDITOR.style} instances are accepted. Required properties * are not allowed in this format. * * Example: * * 'img[src,alt](foo)' // Correct rule. * 'ol, ul(!foo)' // Incorrect rule. Multiple elements and required * // properties are not supported. * * @since 4.1 * @class CKEDITOR.filter.contentRule * @abstract */ /** * Interface that may be automatically implemented by any * instance of any class which has at least the `name` property and * can be meant as an editor feature. * * For example: * * * "Bold" command, button, and keystroke &ndash; it does not mean exactly * `<strong>` or `<b>` but just the ability to create bold text. * * "Format" drop-down list &ndash; it also does not imply any HTML tag. * * "Link" command, button, and keystroke. * * "Image" command, button, and dialog window. * * Thus most often a feature is an instance of one of the following classes: * * * {@link CKEDITOR.command} * * {@link CKEDITOR.ui.button} * * {@link CKEDITOR.ui.richCombo} * * None of them have a `name` property explicitly defined, but * it is set by {@link CKEDITOR.editor#addCommand} and {@link CKEDITOR.ui#add}. * * During editor initialization all features that the editor should activate * should be passed to {@link CKEDITOR.editor#addFeature} (shorthand for {@link CKEDITOR.filter#addFeature}). * * This method checks if a feature can be activated (see {@link #requiredContent}) and if yes, * then it registers allowed content rules required by this feature (see {@link #allowedContent}) along * with two kinds of transformations: {@link #contentForms} and {@link #contentTransformations}. * * By default all buttons that are included in [toolbar layout configuration](#!/guide/dev_toolbar) * are checked and registered with {@link CKEDITOR.editor#addFeature}, all styles available in the * 'Format' and 'Styles' drop-down lists are checked and registered too and so on. * * @since 4.1 * @class CKEDITOR.feature * @abstract */ /** * HTML code that can be generated by this feature. * * For example a basic image feature (image button displaying the image dialog window) * may allow `'img[!src,alt,width,height]'`. * * During the feature activation this value is passed to {@link CKEDITOR.filter#allow}. * * @property {CKEDITOR.filter.allowedContentRules} [allowedContent=null] */ /** * Minimal HTML code that this feature must be allowed to * generate in order to work. * * For example a basic image feature (image button displaying the image dialog window) * needs `'img[src,alt]'` in order to be activated. * * During the feature validation this value is passed to {@link CKEDITOR.filter#check}. * * If this value is not provided, a feature will be always activated. * * @property {CKEDITOR.filter.contentRule} [requiredContent=null] */ /** * The name of the feature. * * It is used for example to identify which {@link CKEDITOR.filter#allowedContent} * rule was added for which feature. * * @property {String} name */ /** * Feature content forms to be registered in the {@link CKEDITOR.editor#filter} * during the feature activation. * * See {@link CKEDITOR.filter#addContentForms} for more details. * * @property [contentForms=null] */ /** * Transformations (usually for content generated by this feature, but not necessarily) * that will be registered in the {@link CKEDITOR.editor#filter} during the feature activation. * * See {@link CKEDITOR.filter#addTransformations} for more details. * * @property [contentTransformations=null] */ /** * Returns a feature that this feature needs to register. * * In some cases, during activation, one feature may need to register * another feature. For example a {@link CKEDITOR.ui.button} often registers * a related command. See {@link CKEDITOR.ui.button#toFeature}. * * This method is executed when a feature is passed to the {@link CKEDITOR.editor#addFeature}. * * @method toFeature * @returns {CKEDITOR.feature} */