]>
Commit | Line | Data |
---|---|---|
7adcb81e IB |
1 | /** |
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | |
4 | */ | |
5 | ||
6 | ( function() { | |
7 | 'use strict'; | |
8 | ||
9 | var DTD = CKEDITOR.dtd, | |
10 | // processElement flag - means that element has been somehow modified. | |
11 | FILTER_ELEMENT_MODIFIED = 1, | |
12 | // processElement flag - meaning explained in CKEDITOR.FILTER_SKIP_TREE doc. | |
13 | FILTER_SKIP_TREE = 2, | |
14 | copy = CKEDITOR.tools.copy, | |
15 | trim = CKEDITOR.tools.trim, | |
16 | TEST_VALUE = 'cke-test', | |
17 | enterModeTags = [ '', 'p', 'br', 'div' ]; | |
18 | ||
19 | /** | |
20 | * A flag indicating that the current element and all its ancestors | |
21 | * should not be filtered. | |
22 | * | |
23 | * See {@link CKEDITOR.filter#addElementCallback} for more details. | |
24 | * | |
25 | * @since 4.4 | |
26 | * @readonly | |
27 | * @property {Number} [=2] | |
28 | * @member CKEDITOR | |
29 | */ | |
30 | CKEDITOR.FILTER_SKIP_TREE = FILTER_SKIP_TREE; | |
31 | ||
32 | /** | |
33 | * Highly configurable class which implements input data filtering mechanisms | |
34 | * and core functions used for the activation of editor features. | |
35 | * | |
36 | * A filter instance is always available under the {@link CKEDITOR.editor#filter} | |
37 | * property and is used by the editor in its core features like filtering input data, | |
38 | * applying data transformations, validating whether a feature may be enabled for | |
39 | * the current setup. It may be configured in two ways: | |
40 | * | |
41 | * * By the user, with the {@link CKEDITOR.config#allowedContent} setting. | |
42 | * * Automatically, by loaded features (toolbar items, commands, etc.). | |
43 | * | |
44 | * In both cases additional allowed content rules may be added by | |
45 | * setting the {@link CKEDITOR.config#extraAllowedContent} | |
46 | * configuration option. | |
47 | * | |
48 | * **Note**: Filter rules will be extended with the following elements | |
49 | * depending on the {@link CKEDITOR.config#enterMode} and | |
50 | * {@link CKEDITOR.config#shiftEnterMode} settings: | |
51 | * | |
52 | * * `'p'` – for {@link CKEDITOR#ENTER_P}, | |
53 | * * `'div'` – for {@link CKEDITOR#ENTER_DIV}, | |
54 | * * `'br'` – for {@link CKEDITOR#ENTER_BR}. | |
55 | * | |
56 | * **Read more** about the Advanced Content Filter in [guides](#!/guide/dev_advanced_content_filter). | |
57 | * | |
58 | * Filter may also be used as a standalone instance by passing | |
59 | * {@link CKEDITOR.filter.allowedContentRules} instead of {@link CKEDITOR.editor} | |
60 | * to the constructor: | |
61 | * | |
62 | * var filter = new CKEDITOR.filter( 'b' ); | |
63 | * | |
64 | * filter.check( 'b' ); // -> true | |
65 | * filter.check( 'i' ); // -> false | |
66 | * filter.allow( 'i' ); | |
67 | * filter.check( 'i' ); // -> true | |
68 | * | |
69 | * @since 4.1 | |
70 | * @class | |
71 | * @constructor Creates a filter class instance. | |
72 | * @param {CKEDITOR.editor/CKEDITOR.filter.allowedContentRules} editorOrRules | |
73 | */ | |
74 | CKEDITOR.filter = function( editorOrRules ) { | |
75 | /** | |
76 | * Whether custom {@link CKEDITOR.config#allowedContent} was set. | |
77 | * | |
78 | * This property does not apply to the standalone filter. | |
79 | * | |
80 | * @readonly | |
81 | * @property {Boolean} customConfig | |
82 | */ | |
83 | ||
84 | /** | |
85 | * Array of rules added by the {@link #allow} method (including those | |
86 | * loaded from {@link CKEDITOR.config#allowedContent} and | |
87 | * {@link CKEDITOR.config#extraAllowedContent}). | |
88 | * | |
89 | * Rules in this array are in unified allowed content rules format. | |
90 | * | |
91 | * This property is useful for debugging issues with rules string parsing | |
92 | * or for checking which rules were automatically added by editor features. | |
93 | * | |
94 | * @readonly | |
95 | */ | |
96 | this.allowedContent = []; | |
97 | ||
98 | /** | |
99 | * Array of rules added by the {@link #disallow} method (including those | |
100 | * loaded from {@link CKEDITOR.config#disallowedContent}). | |
101 | * | |
102 | * Rules in this array are in unified disallowed content rules format. | |
103 | * | |
104 | * This property is useful for debugging issues with rules string parsing | |
105 | * or for checking which rules were automatically added by editor features. | |
106 | * | |
107 | * @since 4.4 | |
108 | * @readonly | |
109 | */ | |
110 | this.disallowedContent = []; | |
111 | ||
112 | /** | |
113 | * Array of element callbacks. See {@link #addElementCallback}. | |
114 | * | |
115 | * @readonly | |
116 | * @property {Function[]} [=null] | |
117 | */ | |
118 | this.elementCallbacks = null; | |
119 | ||
120 | /** | |
121 | * Whether the filter is disabled. | |
122 | * | |
123 | * To disable the filter, set {@link CKEDITOR.config#allowedContent} to `true` | |
124 | * or use the {@link #disable} method. | |
125 | * | |
126 | * @readonly | |
127 | */ | |
128 | this.disabled = false; | |
129 | ||
130 | /** | |
131 | * Editor instance if not a standalone filter. | |
132 | * | |
133 | * @readonly | |
134 | * @property {CKEDITOR.editor} [=null] | |
135 | */ | |
136 | this.editor = null; | |
137 | ||
138 | /** | |
139 | * Filter's unique id. It can be used to find filter instance in | |
140 | * {@link CKEDITOR.filter#instances CKEDITOR.filter.instance} object. | |
141 | * | |
142 | * @since 4.3 | |
143 | * @readonly | |
144 | * @property {Number} id | |
145 | */ | |
146 | this.id = CKEDITOR.tools.getNextNumber(); | |
147 | ||
148 | this._ = { | |
149 | // Optimized allowed content rules. | |
150 | allowedRules: { | |
151 | elements: {}, | |
152 | generic: [] | |
153 | }, | |
154 | // Optimized disallowed content rules. | |
155 | disallowedRules: { | |
156 | elements: {}, | |
157 | generic: [] | |
158 | }, | |
159 | // Object: element name => array of transformations groups. | |
160 | transformations: {}, | |
161 | cachedTests: {} | |
162 | }; | |
163 | ||
164 | // Register filter instance. | |
165 | CKEDITOR.filter.instances[ this.id ] = this; | |
166 | ||
167 | if ( editorOrRules instanceof CKEDITOR.editor ) { | |
168 | var editor = this.editor = editorOrRules; | |
169 | this.customConfig = true; | |
170 | ||
171 | var allowedContent = editor.config.allowedContent; | |
172 | ||
173 | // Disable filter completely by setting config.allowedContent = true. | |
174 | if ( allowedContent === true ) { | |
175 | this.disabled = true; | |
176 | return; | |
177 | } | |
178 | ||
179 | if ( !allowedContent ) | |
180 | this.customConfig = false; | |
181 | ||
182 | this.allow( allowedContent, 'config', 1 ); | |
183 | this.allow( editor.config.extraAllowedContent, 'extra', 1 ); | |
184 | ||
185 | // Enter modes should extend filter rules (ENTER_P adds 'p' rule, etc.). | |
186 | this.allow( enterModeTags[ editor.enterMode ] + ' ' + enterModeTags[ editor.shiftEnterMode ], 'default', 1 ); | |
187 | ||
188 | this.disallow( editor.config.disallowedContent ); | |
189 | } | |
190 | // Rules object passed in editorOrRules argument - initialize standalone filter. | |
191 | else { | |
192 | this.customConfig = false; | |
193 | this.allow( editorOrRules, 'default', 1 ); | |
194 | } | |
195 | }; | |
196 | ||
197 | /** | |
198 | * Object containing all filter instances stored under their | |
199 | * {@link #id} properties. | |
200 | * | |
201 | * var filter = new CKEDITOR.filter( 'p' ); | |
202 | * filter === CKEDITOR.filter.instances[ filter.id ]; | |
203 | * | |
204 | * @since 4.3 | |
205 | * @static | |
206 | * @property instances | |
207 | */ | |
208 | CKEDITOR.filter.instances = {}; | |
209 | ||
210 | CKEDITOR.filter.prototype = { | |
211 | /** | |
212 | * Adds allowed content rules to the filter. | |
213 | * | |
214 | * Read about rules formats in [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). | |
215 | * | |
216 | * // Add a basic rule for custom image feature (e.g. 'MyImage' button). | |
217 | * editor.filter.allow( 'img[!src,alt]', 'MyImage' ); | |
218 | * | |
219 | * // Add rules for two header styles allowed by 'HeadersCombo'. | |
220 | * var header1Style = new CKEDITOR.style( { element: 'h1' } ), | |
221 | * header2Style = new CKEDITOR.style( { element: 'h2' } ); | |
222 | * editor.filter.allow( [ header1Style, header2Style ], 'HeadersCombo' ); | |
223 | * | |
224 | * @param {CKEDITOR.filter.allowedContentRules} newRules Rule(s) to be added. | |
225 | * @param {String} [featureName] Name of a feature that allows this content (most often plugin/button/command name). | |
226 | * @param {Boolean} [overrideCustom] By default this method will reject any rules | |
227 | * if {@link CKEDITOR.config#allowedContent} is defined to avoid overriding it. | |
228 | * Pass `true` to force rules addition. | |
229 | * @returns {Boolean} Whether the rules were accepted. | |
230 | */ | |
231 | allow: function( newRules, featureName, overrideCustom ) { | |
232 | // Check arguments and constraints. Clear cache. | |
233 | if ( !beforeAddingRule( this, newRules, overrideCustom ) ) | |
234 | return false; | |
235 | ||
236 | var i, ret; | |
237 | ||
238 | if ( typeof newRules == 'string' ) | |
239 | newRules = parseRulesString( newRules ); | |
240 | else if ( newRules instanceof CKEDITOR.style ) { | |
241 | // If style has the cast method defined, use it and abort. | |
242 | if ( newRules.toAllowedContentRules ) | |
243 | return this.allow( newRules.toAllowedContentRules( this.editor ), featureName, overrideCustom ); | |
244 | ||
245 | newRules = convertStyleToRules( newRules ); | |
246 | } else if ( CKEDITOR.tools.isArray( newRules ) ) { | |
247 | for ( i = 0; i < newRules.length; ++i ) | |
248 | ret = this.allow( newRules[ i ], featureName, overrideCustom ); | |
249 | return ret; // Return last status. | |
250 | } | |
251 | ||
252 | addAndOptimizeRules( this, newRules, featureName, this.allowedContent, this._.allowedRules ); | |
253 | ||
254 | return true; | |
255 | }, | |
256 | ||
257 | /** | |
258 | * Applies this filter to passed {@link CKEDITOR.htmlParser.fragment} or {@link CKEDITOR.htmlParser.element}. | |
259 | * The result of filtering is a DOM tree without disallowed content. | |
260 | * | |
261 | * // Create standalone filter passing 'p' and 'b' elements. | |
262 | * var filter = new CKEDITOR.filter( 'p b' ), | |
263 | * // Parse HTML string to pseudo DOM structure. | |
264 | * fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p><b>foo</b> <i>bar</i></p>' ), | |
265 | * writer = new CKEDITOR.htmlParser.basicWriter(); | |
266 | * | |
267 | * filter.applyTo( fragment ); | |
268 | * fragment.writeHtml( writer ); | |
269 | * writer.getHtml(); // -> '<p><b>foo</b> bar</p>' | |
270 | * | |
271 | * @param {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} fragment Node to be filtered. | |
272 | * @param {Boolean} [toHtml] Set to `true` if the filter is used together with {@link CKEDITOR.htmlDataProcessor#toHtml}. | |
273 | * @param {Boolean} [transformOnly] If set to `true` only transformations will be applied. Content | |
274 | * will not be filtered with allowed content rules. | |
275 | * @param {Number} [enterMode] Enter mode used by the filter when deciding how to strip disallowed element. | |
276 | * Defaults to {@link CKEDITOR.editor#activeEnterMode} for a editor's filter or to {@link CKEDITOR#ENTER_P} for standalone filter. | |
277 | * @returns {Boolean} Whether some part of the `fragment` was removed by the filter. | |
278 | */ | |
279 | applyTo: function( fragment, toHtml, transformOnly, enterMode ) { | |
280 | if ( this.disabled ) | |
281 | return false; | |
282 | ||
283 | var that = this, | |
284 | toBeRemoved = [], | |
285 | protectedRegexs = this.editor && this.editor.config.protectedSource, | |
286 | processRetVal, | |
287 | isModified = false, | |
288 | filterOpts = { | |
289 | doFilter: !transformOnly, | |
290 | doTransform: true, | |
291 | doCallbacks: true, | |
292 | toHtml: toHtml | |
293 | }; | |
294 | ||
295 | // Filter all children, skip root (fragment or editable-like wrapper used by data processor). | |
296 | fragment.forEach( function( el ) { | |
297 | if ( el.type == CKEDITOR.NODE_ELEMENT ) { | |
298 | // Do not filter element with data-cke-filter="off" and all their descendants. | |
299 | if ( el.attributes[ 'data-cke-filter' ] == 'off' ) | |
300 | return false; | |
301 | ||
302 | // (#10260) Don't touch elements like spans with data-cke-* attribute since they're | |
303 | // responsible e.g. for placing markers, bookmarks, odds and stuff. | |
304 | // We love 'em and we don't wanna lose anything during the filtering. | |
305 | // '|' is to avoid tricky joints like data-="foo" + cke-="bar". Yes, they're possible. | |
306 | // | |
307 | // NOTE: data-cke-* assigned elements are preserved only when filter is used with | |
308 | // htmlDataProcessor.toHtml because we don't want to protect them when outputting data | |
309 | // (toDataFormat). | |
310 | if ( toHtml && el.name == 'span' && ~CKEDITOR.tools.objectKeys( el.attributes ).join( '|' ).indexOf( 'data-cke-' ) ) | |
311 | return; | |
312 | ||
313 | processRetVal = processElement( that, el, toBeRemoved, filterOpts ); | |
314 | if ( processRetVal & FILTER_ELEMENT_MODIFIED ) | |
315 | isModified = true; | |
316 | else if ( processRetVal & FILTER_SKIP_TREE ) | |
317 | return false; | |
318 | } | |
319 | else if ( el.type == CKEDITOR.NODE_COMMENT && el.value.match( /^\{cke_protected\}(?!\{C\})/ ) ) { | |
320 | if ( !processProtectedElement( that, el, protectedRegexs, filterOpts ) ) | |
321 | toBeRemoved.push( el ); | |
322 | } | |
323 | }, null, true ); | |
324 | ||
325 | if ( toBeRemoved.length ) | |
326 | isModified = true; | |
327 | ||
328 | var node, element, check, | |
329 | toBeChecked = [], | |
330 | enterTag = enterModeTags[ enterMode || ( this.editor ? this.editor.enterMode : CKEDITOR.ENTER_P ) ], | |
331 | parentDtd; | |
332 | ||
333 | // Remove elements in reverse order - from leaves to root, to avoid conflicts. | |
334 | while ( ( node = toBeRemoved.pop() ) ) { | |
335 | if ( node.type == CKEDITOR.NODE_ELEMENT ) | |
336 | removeElement( node, enterTag, toBeChecked ); | |
337 | // This is a comment securing rejected element - remove it completely. | |
338 | else | |
339 | node.remove(); | |
340 | } | |
341 | ||
342 | // Check elements that have been marked as possibly invalid. | |
343 | while ( ( check = toBeChecked.pop() ) ) { | |
344 | element = check.el; | |
345 | // Element has been already removed. | |
346 | if ( !element.parent ) | |
347 | continue; | |
348 | ||
349 | // Handle custom elements as inline elements (#12683). | |
350 | parentDtd = DTD[ element.parent.name ] || DTD.span; | |
351 | ||
352 | switch ( check.check ) { | |
353 | // Check if element itself is correct. | |
354 | case 'it': | |
355 | // Check if element included in $removeEmpty has no children. | |
356 | if ( DTD.$removeEmpty[ element.name ] && !element.children.length ) | |
357 | removeElement( element, enterTag, toBeChecked ); | |
358 | // Check if that is invalid element. | |
359 | else if ( !validateElement( element ) ) | |
360 | removeElement( element, enterTag, toBeChecked ); | |
361 | break; | |
362 | ||
363 | // Check if element is in correct context. If not - remove element. | |
364 | case 'el-up': | |
365 | // Check if e.g. li is a child of body after ul has been removed. | |
366 | if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) | |
367 | removeElement( element, enterTag, toBeChecked ); | |
368 | break; | |
369 | ||
370 | // Check if element is in correct context. If not - remove parent. | |
371 | case 'parent-down': | |
372 | if ( element.parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && !parentDtd[ element.name ] ) | |
373 | removeElement( element.parent, enterTag, toBeChecked ); | |
374 | break; | |
375 | } | |
376 | } | |
377 | ||
378 | return isModified; | |
379 | }, | |
380 | ||
381 | /** | |
382 | * Checks whether a {@link CKEDITOR.feature} can be enabled. Unlike {@link #addFeature}, | |
383 | * this method always checks the feature, even when the default configuration | |
384 | * for {@link CKEDITOR.config#allowedContent} is used. | |
385 | * | |
386 | * // TODO example | |
387 | * | |
388 | * @param {CKEDITOR.feature} feature The feature to be tested. | |
389 | * @returns {Boolean} Whether this feature can be enabled. | |
390 | */ | |
391 | checkFeature: function( feature ) { | |
392 | if ( this.disabled ) | |
393 | return true; | |
394 | ||
395 | if ( !feature ) | |
396 | return true; | |
397 | ||
398 | // Some features may want to register other features. | |
399 | // E.g. a button may return a command bound to it. | |
400 | if ( feature.toFeature ) | |
401 | feature = feature.toFeature( this.editor ); | |
402 | ||
403 | return !feature.requiredContent || this.check( feature.requiredContent ); | |
404 | }, | |
405 | ||
406 | /** | |
407 | * Disables Advanced Content Filter. | |
408 | * | |
409 | * This method is meant to be used by plugins which are not | |
410 | * compatible with the filter and in other cases in which the filter | |
411 | * has to be disabled during the initialization phase or runtime. | |
412 | * | |
413 | * In other cases the filter can be disabled by setting | |
414 | * {@link CKEDITOR.config#allowedContent} to `true`. | |
415 | */ | |
416 | disable: function() { | |
417 | this.disabled = true; | |
418 | }, | |
419 | ||
420 | /** | |
421 | * Adds disallowed content rules to the filter. | |
422 | * | |
423 | * Read about rules formats in the [Allowed Content Rules guide](#!/guide/dev_allowed_content_rules). | |
424 | * | |
425 | * // Disallow all styles on the image elements. | |
426 | * editor.filter.disallow( 'img{*}' ); | |
427 | * | |
428 | * // Disallow all span and div elements. | |
429 | * editor.filter.disallow( 'span div' ); | |
430 | * | |
431 | * @since 4.4 | |
432 | * @param {CKEDITOR.filter.disallowedContentRules} newRules Rule(s) to be added. | |
433 | */ | |
434 | disallow: function( newRules ) { | |
435 | // Check arguments and constraints. Clear cache. | |
436 | // Note: we pass true in the 3rd argument, because disallow() should never | |
437 | // be blocked by custom configuration. | |
438 | if ( !beforeAddingRule( this, newRules, true ) ) | |
439 | return false; | |
440 | ||
441 | if ( typeof newRules == 'string' ) | |
442 | newRules = parseRulesString( newRules ); | |
443 | ||
444 | addAndOptimizeRules( this, newRules, null, this.disallowedContent, this._.disallowedRules ); | |
445 | ||
446 | return true; | |
447 | }, | |
448 | ||
449 | /** | |
450 | * Adds an array of {@link CKEDITOR.feature} content forms. All forms | |
451 | * will then be transformed to the first form which is allowed by the filter. | |
452 | * | |
453 | * editor.filter.allow( 'i; span{!font-style}' ); | |
454 | * editor.filter.addContentForms( [ | |
455 | * 'em', | |
456 | * 'i', | |
457 | * [ 'span', function( el ) { | |
458 | * return el.styles[ 'font-style' ] == 'italic'; | |
459 | * } ] | |
460 | * ] ); | |
461 | * // Now <em> and <span style="font-style:italic"> will be replaced with <i> | |
462 | * // because this is the first allowed form. | |
463 | * // <span> is allowed too, but it is the last form and | |
464 | * // additionaly, the editor cannot transform an element based on | |
465 | * // the array+function form). | |
466 | * | |
467 | * This method is used by the editor to register {@link CKEDITOR.feature#contentForms} | |
468 | * when adding a feature with {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. | |
469 | * | |
470 | * @param {Array} forms The content forms of a feature. | |
471 | */ | |
472 | addContentForms: function( forms ) { | |
473 | if ( this.disabled ) | |
474 | return; | |
475 | ||
476 | if ( !forms ) | |
477 | return; | |
478 | ||
479 | var i, form, | |
480 | transfGroups = [], | |
481 | preferredForm; | |
482 | ||
483 | // First, find preferred form - this is, first allowed. | |
484 | for ( i = 0; i < forms.length && !preferredForm; ++i ) { | |
485 | form = forms[ i ]; | |
486 | ||
487 | // Check only strings and styles - array format isn't supported by #check(). | |
488 | if ( ( typeof form == 'string' || form instanceof CKEDITOR.style ) && this.check( form ) ) | |
489 | preferredForm = form; | |
490 | } | |
491 | ||
492 | // This feature doesn't have preferredForm, so ignore it. | |
493 | if ( !preferredForm ) | |
494 | return; | |
495 | ||
496 | for ( i = 0; i < forms.length; ++i ) | |
497 | transfGroups.push( getContentFormTransformationGroup( forms[ i ], preferredForm ) ); | |
498 | ||
499 | this.addTransformations( transfGroups ); | |
500 | }, | |
501 | ||
502 | /** | |
503 | * Adds a callback which will be executed on every element | |
504 | * that the filter reaches when filtering, before the element is filtered. | |
505 | * | |
506 | * By returning {@link CKEDITOR#FILTER_SKIP_TREE} it is possible to | |
507 | * skip filtering of the current element and all its ancestors. | |
508 | * | |
509 | * editor.filter.addElementCallback( function( el ) { | |
510 | * if ( el.hasClass( 'protected' ) ) | |
511 | * return CKEDITOR.FILTER_SKIP_TREE; | |
512 | * } ); | |
513 | * | |
514 | * **Note:** At this stage the element passed to the callback does not | |
515 | * contain `attributes`, `classes`, and `styles` properties which are available | |
516 | * temporarily on later stages of the filtering process. Therefore you need to | |
517 | * use the pure {@link CKEDITOR.htmlParser.element} interface. | |
518 | * | |
519 | * @since 4.4 | |
520 | * @param {Function} callback The callback to be executed. | |
521 | */ | |
522 | addElementCallback: function( callback ) { | |
523 | // We want to keep it a falsy value, to speed up finding whether there are any callbacks. | |
524 | if ( !this.elementCallbacks ) | |
525 | this.elementCallbacks = []; | |
526 | ||
527 | this.elementCallbacks.push( callback ); | |
528 | }, | |
529 | ||
530 | /** | |
531 | * Checks whether a feature can be enabled for the HTML restrictions in place | |
532 | * for the current CKEditor instance, based on the HTML code the feature might | |
533 | * generate and the minimal HTML code the feature needs to be able to generate. | |
534 | * | |
535 | * // TODO example | |
536 | * | |
537 | * @param {CKEDITOR.feature} feature | |
538 | * @returns {Boolean} Whether this feature can be enabled. | |
539 | */ | |
540 | addFeature: function( feature ) { | |
541 | if ( this.disabled ) | |
542 | return true; | |
543 | ||
544 | if ( !feature ) | |
545 | return true; | |
546 | ||
547 | // Some features may want to register other features. | |
548 | // E.g. a button may return a command bound to it. | |
549 | if ( feature.toFeature ) | |
550 | feature = feature.toFeature( this.editor ); | |
551 | ||
552 | // If default configuration (will be checked inside #allow()), | |
553 | // then add allowed content rules. | |
554 | this.allow( feature.allowedContent, feature.name ); | |
555 | ||
556 | this.addTransformations( feature.contentTransformations ); | |
557 | this.addContentForms( feature.contentForms ); | |
558 | ||
559 | // If custom configuration or any DACRs, then check if required content is allowed. | |
560 | if ( feature.requiredContent && ( this.customConfig || this.disallowedContent.length ) ) | |
561 | return this.check( feature.requiredContent ); | |
562 | ||
563 | return true; | |
564 | }, | |
565 | ||
566 | /** | |
567 | * Adds an array of content transformation groups. One group | |
568 | * may contain many transformation rules, but only the first | |
569 | * matching rule in a group is executed. | |
570 | * | |
571 | * A single transformation rule is an object with four properties: | |
572 | * | |
573 | * * `check` (optional) – if set and {@link CKEDITOR.filter} does | |
574 | * not accept this {@link CKEDITOR.filter.contentRule}, this transformation rule | |
575 | * will not be executed (it does not *match*). This value is passed | |
576 | * to {@link #check}. | |
577 | * * `element` (optional) – this string property tells the filter on which | |
578 | * element this transformation can be run. It is optional, because | |
579 | * the element name can be obtained from `check` (if it is a String format) | |
580 | * or `left` (if it is a {@link CKEDITOR.style} instance). | |
581 | * * `left` (optional) – a function accepting an element or a {@link CKEDITOR.style} | |
582 | * instance verifying whether the transformation should be | |
583 | * executed on this specific element. If it returns `false` or if an element | |
584 | * does not match this style, this transformation rule does not *match*. | |
585 | * * `right` – a function accepting an element and {@link CKEDITOR.filter.transformationsTools} | |
586 | * or a string containing the name of the {@link CKEDITOR.filter.transformationsTools} method | |
587 | * that should be called on an element. | |
588 | * | |
589 | * A shorthand format is also available. A transformation rule can be defined by | |
590 | * a single string `'check:right'`. The string before `':'` will be used as | |
591 | * the `check` property and the second part as the `right` property. | |
592 | * | |
593 | * Transformation rules can be grouped. The filter will try to apply | |
594 | * the first rule in a group. If it *matches*, the filter will ignore subsequent rules and | |
595 | * will move to the next group. If it does not *match*, the next rule will be checked. | |
596 | * | |
597 | * Examples: | |
598 | * | |
599 | * editor.filter.addTransformations( [ | |
600 | * // First group. | |
601 | * [ | |
602 | * // First rule. If table{width} is allowed, it | |
603 | * // executes {@link CKEDITOR.filter.transformationsTools#sizeToStyle} on a table element. | |
604 | * 'table{width}: sizeToStyle', | |
605 | * // Second rule should not be executed if the first was. | |
606 | * 'table[width]: sizeToAttribute' | |
607 | * ], | |
608 | * // Second group. | |
609 | * [ | |
610 | * // This rule will add the foo="1" attribute to all images that | |
611 | * // do not have it. | |
612 | * { | |
613 | * element: 'img', | |
614 | * left: function( el ) { | |
615 | * return !el.attributes.foo; | |
616 | * }, | |
617 | * right: function( el, tools ) { | |
618 | * el.attributes.foo = '1'; | |
619 | * } | |
620 | * } | |
621 | * ] | |
622 | * ] ); | |
623 | * | |
624 | * // Case 1: | |
625 | * // config.allowedContent = 'table{height,width}; tr td'. | |
626 | * // | |
627 | * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' | |
628 | * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' | |
629 | * | |
630 | * // Case 2: | |
631 | * // config.allowedContent = 'table[height,width]; tr td'. | |
632 | * // | |
633 | * // '<table style="height:100px; width:200px">...</table>' -> '<table height="100" width="200">...</table>' | |
634 | * // '<table height="100" width="200">...</table>' -> '<table height="100" width="200"">...</table>' | |
635 | * | |
636 | * // Case 3: | |
637 | * // config.allowedContent = 'table{width,height}[height,width]; tr td'. | |
638 | * // | |
639 | * // '<table style="height:100px; width:200px">...</table>' -> '<table style="height:100px; width:200px">...</table>' | |
640 | * // '<table height="100" width="200">...</table>' -> '<table style="height:100px; width:200px">...</table>' | |
641 | * // | |
642 | * // Note: Both forms are allowed (size set by style and by attributes), but only | |
643 | * // the first transformation is applied — the size is always transformed to a style. | |
644 | * // This is because only the first transformation matching allowed content rules is applied. | |
645 | * | |
646 | * This method is used by the editor to add {@link CKEDITOR.feature#contentTransformations} | |
647 | * when adding a feature by {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. | |
648 | * | |
649 | * @param {Array} transformations | |
650 | */ | |
651 | addTransformations: function( transformations ) { | |
652 | if ( this.disabled ) | |
653 | return; | |
654 | ||
655 | if ( !transformations ) | |
656 | return; | |
657 | ||
658 | var optimized = this._.transformations, | |
659 | group, i; | |
660 | ||
661 | for ( i = 0; i < transformations.length; ++i ) { | |
662 | group = optimizeTransformationsGroup( transformations[ i ] ); | |
663 | ||
664 | if ( !optimized[ group.name ] ) | |
665 | optimized[ group.name ] = []; | |
666 | ||
667 | optimized[ group.name ].push( group.rules ); | |
668 | } | |
669 | }, | |
670 | ||
671 | /** | |
672 | * Checks whether the content defined in the `test` argument is allowed | |
673 | * by this filter. | |
674 | * | |
675 | * If `strictCheck` is set to `false` (default value), this method checks | |
676 | * if all parts of the `test` (styles, attributes, and classes) are | |
677 | * accepted by the filter. If `strictCheck` is set to `true`, the test | |
678 | * must also contain the required attributes, styles, and classes. | |
679 | * | |
680 | * For example: | |
681 | * | |
682 | * // Rule: 'img[!src,alt]'. | |
683 | * filter.check( 'img[alt]' ); // -> true | |
684 | * filter.check( 'img[alt]', true, true ); // -> false | |
685 | * | |
686 | * Second `check()` call returned `false` because `src` is required. | |
687 | * | |
688 | * **Note:** The `test` argument is of {@link CKEDITOR.filter.contentRule} type, which is | |
689 | * a limited version of {@link CKEDITOR.filter.allowedContentRules}. Read more about it | |
690 | * in the {@link CKEDITOR.filter.contentRule}'s documentation. | |
691 | * | |
692 | * @param {CKEDITOR.filter.contentRule} test | |
693 | * @param {Boolean} [applyTransformations=true] Whether to use registered transformations. | |
694 | * @param {Boolean} [strictCheck] Whether the filter should check if an element with exactly | |
695 | * these properties is allowed. | |
696 | * @returns {Boolean} Returns `true` if the content is allowed. | |
697 | */ | |
698 | check: function( test, applyTransformations, strictCheck ) { | |
699 | if ( this.disabled ) | |
700 | return true; | |
701 | ||
702 | // If rules are an array, expand it and return the logical OR value of | |
703 | // the rules. | |
704 | if ( CKEDITOR.tools.isArray( test ) ) { | |
705 | for ( var i = test.length ; i-- ; ) { | |
706 | if ( this.check( test[ i ], applyTransformations, strictCheck ) ) | |
707 | return true; | |
708 | } | |
709 | return false; | |
710 | } | |
711 | ||
712 | var element, result, cacheKey; | |
713 | ||
714 | if ( typeof test == 'string' ) { | |
715 | cacheKey = test + '<' + ( applyTransformations === false ? '0' : '1' ) + ( strictCheck ? '1' : '0' ) + '>'; | |
716 | ||
717 | // Check if result of this check hasn't been already cached. | |
718 | if ( cacheKey in this._.cachedChecks ) | |
719 | return this._.cachedChecks[ cacheKey ]; | |
720 | ||
721 | // Create test element from string. | |
722 | element = mockElementFromString( test ); | |
723 | } else { | |
724 | // Create test element from CKEDITOR.style. | |
725 | element = mockElementFromStyle( test ); | |
726 | } | |
727 | ||
728 | // Make a deep copy. | |
729 | var clone = CKEDITOR.tools.clone( element ), | |
730 | toBeRemoved = [], | |
731 | transformations; | |
732 | ||
733 | // Apply transformations to original element. | |
734 | // Transformations will be applied to clone by the filter function. | |
735 | if ( applyTransformations !== false && ( transformations = this._.transformations[ element.name ] ) ) { | |
736 | for ( i = 0; i < transformations.length; ++i ) | |
737 | applyTransformationsGroup( this, element, transformations[ i ] ); | |
738 | ||
739 | // Transformations could modify styles or classes, so they need to be copied | |
740 | // to attributes object. | |
741 | updateAttributes( element ); | |
742 | } | |
743 | ||
744 | // Filter clone of mocked element. | |
745 | processElement( this, clone, toBeRemoved, { | |
746 | doFilter: true, | |
747 | doTransform: applyTransformations !== false, | |
748 | skipRequired: !strictCheck, | |
749 | skipFinalValidation: !strictCheck | |
750 | } ); | |
751 | ||
752 | // Element has been marked for removal. | |
753 | if ( toBeRemoved.length > 0 ) { | |
754 | result = false; | |
755 | // Compare only left to right, because clone may be only trimmed version of original element. | |
756 | } else if ( !CKEDITOR.tools.objectCompare( element.attributes, clone.attributes, true ) ) { | |
757 | result = false; | |
758 | } else { | |
759 | result = true; | |
760 | } | |
761 | ||
762 | // Cache result of this test - we can build cache only for string tests. | |
763 | if ( typeof test == 'string' ) | |
764 | this._.cachedChecks[ cacheKey ] = result; | |
765 | ||
766 | return result; | |
767 | }, | |
768 | ||
769 | /** | |
770 | * Returns first enter mode allowed by this filter rules. Modes are checked in `p`, `div`, `br` order. | |
771 | * If none of tags is allowed this method will return {@link CKEDITOR#ENTER_BR}. | |
772 | * | |
773 | * @since 4.3 | |
774 | * @param {Number} defaultMode The default mode which will be checked as the first one. | |
775 | * @param {Boolean} [reverse] Whether to check modes in reverse order (used for shift enter mode). | |
776 | * @returns {Number} Allowed enter mode. | |
777 | */ | |
778 | getAllowedEnterMode: ( function() { | |
779 | var tagsToCheck = [ 'p', 'div', 'br' ], | |
780 | enterModes = { | |
781 | p: CKEDITOR.ENTER_P, | |
782 | div: CKEDITOR.ENTER_DIV, | |
783 | br: CKEDITOR.ENTER_BR | |
784 | }; | |
785 | ||
786 | return function( defaultMode, reverse ) { | |
787 | // Clone the array first. | |
788 | var tags = tagsToCheck.slice(), | |
789 | tag; | |
790 | ||
791 | // Check the default mode first. | |
792 | if ( this.check( enterModeTags[ defaultMode ] ) ) | |
793 | return defaultMode; | |
794 | ||
795 | // If not reverse order, reverse array so we can pop() from it. | |
796 | if ( !reverse ) | |
797 | tags = tags.reverse(); | |
798 | ||
799 | while ( ( tag = tags.pop() ) ) { | |
800 | if ( this.check( tag ) ) | |
801 | return enterModes[ tag ]; | |
802 | } | |
803 | ||
804 | return CKEDITOR.ENTER_BR; | |
805 | }; | |
806 | } )(), | |
807 | ||
808 | /** | |
809 | * Destroys the filter instance and removes it from the global {@link CKEDITOR.filter#instances} object. | |
810 | * | |
811 | * @since 4.4.5 | |
812 | */ | |
813 | destroy: function() { | |
814 | delete CKEDITOR.filter.instances[ this.id ]; | |
815 | // Deleting reference to filter instance should be enough, | |
816 | // but since these are big objects it's safe to clean them up too. | |
817 | delete this._; | |
818 | delete this.allowedContent; | |
819 | delete this.disallowedContent; | |
820 | } | |
821 | }; | |
822 | ||
823 | function addAndOptimizeRules( that, newRules, featureName, standardizedRules, optimizedRules ) { | |
824 | var groupName, rule, | |
825 | rulesToOptimize = []; | |
826 | ||
827 | for ( groupName in newRules ) { | |
828 | rule = newRules[ groupName ]; | |
829 | ||
830 | // { 'p h1': true } => { 'p h1': {} }. | |
831 | if ( typeof rule == 'boolean' ) | |
832 | rule = {}; | |
833 | // { 'p h1': func } => { 'p h1': { match: func } }. | |
834 | else if ( typeof rule == 'function' ) | |
835 | rule = { match: rule }; | |
836 | // Clone (shallow) rule, because we'll modify it later. | |
837 | else | |
838 | rule = copy( rule ); | |
839 | ||
840 | // If this is not an unnamed rule ({ '$1' => { ... } }) | |
841 | // move elements list to property. | |
842 | if ( groupName.charAt( 0 ) != '$' ) | |
843 | rule.elements = groupName; | |
844 | ||
845 | if ( featureName ) | |
846 | rule.featureName = featureName.toLowerCase(); | |
847 | ||
848 | standardizeRule( rule ); | |
849 | ||
850 | // Save rule and remember to optimize it. | |
851 | standardizedRules.push( rule ); | |
852 | rulesToOptimize.push( rule ); | |
853 | } | |
854 | ||
855 | optimizeRules( optimizedRules, rulesToOptimize ); | |
856 | } | |
857 | ||
858 | // Apply ACR to an element. | |
859 | // @param rule | |
860 | // @param element | |
861 | // @param status Object containing status of element's filtering. | |
862 | // @param {Boolean} skipRequired If true don't check if element has all required properties. | |
863 | function applyAllowedRule( rule, element, status, skipRequired ) { | |
864 | // This rule doesn't match this element - skip it. | |
865 | if ( rule.match && !rule.match( element ) ) | |
866 | return; | |
867 | ||
868 | // If element doesn't have all required styles/attrs/classes | |
869 | // this rule doesn't match it. | |
870 | if ( !skipRequired && !hasAllRequired( rule, element ) ) | |
871 | return; | |
872 | ||
873 | // If this rule doesn't validate properties only mark element as valid. | |
874 | if ( !rule.propertiesOnly ) | |
875 | status.valid = true; | |
876 | ||
877 | // Apply rule only when all attrs/styles/classes haven't been marked as valid. | |
878 | if ( !status.allAttributes ) | |
879 | status.allAttributes = applyAllowedRuleToHash( rule.attributes, element.attributes, status.validAttributes ); | |
880 | ||
881 | if ( !status.allStyles ) | |
882 | status.allStyles = applyAllowedRuleToHash( rule.styles, element.styles, status.validStyles ); | |
883 | ||
884 | if ( !status.allClasses ) | |
885 | status.allClasses = applyAllowedRuleToArray( rule.classes, element.classes, status.validClasses ); | |
886 | } | |
887 | ||
888 | // Apply itemsRule to items (only classes are kept in array). | |
889 | // Push accepted items to validItems array. | |
890 | // Return true when all items are valid. | |
891 | function applyAllowedRuleToArray( itemsRule, items, validItems ) { | |
892 | if ( !itemsRule ) | |
893 | return false; | |
894 | ||
895 | // True means that all elements of array are accepted (the asterix was used for classes). | |
896 | if ( itemsRule === true ) | |
897 | return true; | |
898 | ||
899 | for ( var i = 0, l = items.length, item; i < l; ++i ) { | |
900 | item = items[ i ]; | |
901 | if ( !validItems[ item ] ) | |
902 | validItems[ item ] = itemsRule( item ); | |
903 | } | |
904 | ||
905 | return false; | |
906 | } | |
907 | ||
908 | function applyAllowedRuleToHash( itemsRule, items, validItems ) { | |
909 | if ( !itemsRule ) | |
910 | return false; | |
911 | ||
912 | if ( itemsRule === true ) | |
913 | return true; | |
914 | ||
915 | for ( var name in items ) { | |
916 | if ( !validItems[ name ] ) | |
917 | validItems[ name ] = itemsRule( name ); | |
918 | } | |
919 | ||
920 | return false; | |
921 | } | |
922 | ||
923 | // Apply DACR rule to an element. | |
924 | function applyDisallowedRule( rule, element, status ) { | |
925 | // This rule doesn't match this element - skip it. | |
926 | if ( rule.match && !rule.match( element ) ) | |
927 | return; | |
928 | ||
929 | // No properties - it's an element only rule so it disallows entire element. | |
930 | // Early return is handled in filterElement. | |
931 | if ( rule.noProperties ) | |
932 | return false; | |
933 | ||
934 | // Apply rule to attributes, styles and classes. Switch hadInvalid* to true if method returned true. | |
935 | status.hadInvalidAttribute = applyDisallowedRuleToHash( rule.attributes, element.attributes ) || status.hadInvalidAttribute; | |
936 | status.hadInvalidStyle = applyDisallowedRuleToHash( rule.styles, element.styles ) || status.hadInvalidStyle; | |
937 | status.hadInvalidClass = applyDisallowedRuleToArray( rule.classes, element.classes ) || status.hadInvalidClass; | |
938 | } | |
939 | ||
940 | // Apply DACR to items (only classes are kept in array). | |
941 | // @returns {Boolean} True if at least one of items was invalid (disallowed). | |
942 | function applyDisallowedRuleToArray( itemsRule, items ) { | |
943 | if ( !itemsRule ) | |
944 | return false; | |
945 | ||
946 | var hadInvalid = false, | |
947 | allDisallowed = itemsRule === true; | |
948 | ||
949 | for ( var i = items.length; i--; ) { | |
950 | if ( allDisallowed || itemsRule( items[ i ] ) ) { | |
951 | items.splice( i, 1 ); | |
952 | hadInvalid = true; | |
953 | } | |
954 | } | |
955 | ||
956 | return hadInvalid; | |
957 | } | |
958 | ||
959 | // Apply DACR to items (styles and attributes). | |
960 | // @returns {Boolean} True if at least one of items was invalid (disallowed). | |
961 | function applyDisallowedRuleToHash( itemsRule, items ) { | |
962 | if ( !itemsRule ) | |
963 | return false; | |
964 | ||
965 | var hadInvalid = false, | |
966 | allDisallowed = itemsRule === true; | |
967 | ||
968 | for ( var name in items ) { | |
969 | if ( allDisallowed || itemsRule( name ) ) { | |
970 | delete items[ name ]; | |
971 | hadInvalid = true; | |
972 | } | |
973 | } | |
974 | ||
975 | return hadInvalid; | |
976 | } | |
977 | ||
978 | function beforeAddingRule( that, newRules, overrideCustom ) { | |
979 | if ( that.disabled ) | |
980 | return false; | |
981 | ||
982 | // Don't override custom user's configuration if not explicitly requested. | |
983 | if ( that.customConfig && !overrideCustom ) | |
984 | return false; | |
985 | ||
986 | if ( !newRules ) | |
987 | return false; | |
988 | ||
989 | // Clear cache, because new rules could change results of checks. | |
990 | that._.cachedChecks = {}; | |
991 | ||
992 | return true; | |
993 | } | |
994 | ||
995 | // Convert CKEDITOR.style to filter's rule. | |
996 | function convertStyleToRules( style ) { | |
997 | var styleDef = style.getDefinition(), | |
998 | rules = {}, | |
999 | rule, | |
1000 | attrs = styleDef.attributes; | |
1001 | ||
1002 | rules[ styleDef.element ] = rule = { | |
1003 | styles: styleDef.styles, | |
1004 | requiredStyles: styleDef.styles && CKEDITOR.tools.objectKeys( styleDef.styles ) | |
1005 | }; | |
1006 | ||
1007 | if ( attrs ) { | |
1008 | attrs = copy( attrs ); | |
1009 | rule.classes = attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : null; | |
1010 | rule.requiredClasses = rule.classes; | |
1011 | delete attrs[ 'class' ]; | |
1012 | rule.attributes = attrs; | |
1013 | rule.requiredAttributes = attrs && CKEDITOR.tools.objectKeys( attrs ); | |
1014 | } | |
1015 | ||
1016 | return rules; | |
1017 | } | |
1018 | ||
1019 | // Convert all validator formats (string, array, object, boolean) to hash or boolean: | |
1020 | // * true is returned for '*'/true validator, | |
1021 | // * false is returned for empty validator (no validator at all (false/null) or e.g. empty array), | |
1022 | // * object is returned in other cases. | |
1023 | function convertValidatorToHash( validator, delimiter ) { | |
1024 | if ( !validator ) | |
1025 | return false; | |
1026 | ||
1027 | if ( validator === true ) | |
1028 | return validator; | |
1029 | ||
1030 | if ( typeof validator == 'string' ) { | |
1031 | validator = trim( validator ); | |
1032 | if ( validator == '*' ) | |
1033 | return true; | |
1034 | else | |
1035 | return CKEDITOR.tools.convertArrayToObject( validator.split( delimiter ) ); | |
1036 | } | |
1037 | else if ( CKEDITOR.tools.isArray( validator ) ) { | |
1038 | if ( validator.length ) | |
1039 | return CKEDITOR.tools.convertArrayToObject( validator ); | |
1040 | else | |
1041 | return false; | |
1042 | } | |
1043 | // If object. | |
1044 | else { | |
1045 | var obj = {}, | |
1046 | len = 0; | |
1047 | ||
1048 | for ( var i in validator ) { | |
1049 | obj[ i ] = validator[ i ]; | |
1050 | len++; | |
1051 | } | |
1052 | ||
1053 | return len ? obj : false; | |
1054 | } | |
1055 | } | |
1056 | ||
1057 | function executeElementCallbacks( element, callbacks ) { | |
1058 | for ( var i = 0, l = callbacks.length, retVal; i < l; ++i ) { | |
1059 | if ( ( retVal = callbacks[ i ]( element ) ) ) | |
1060 | return retVal; | |
1061 | } | |
1062 | } | |
1063 | ||
1064 | // Extract required properties from "required" validator and "all" properties. | |
1065 | // Remove exclamation marks from "all" properties. | |
1066 | // | |
1067 | // E.g.: | |
1068 | // requiredClasses = { cl1: true } | |
1069 | // (all) classes = { cl1: true, cl2: true, '!cl3': true } | |
1070 | // | |
1071 | // result: | |
1072 | // returned = { cl1: true, cl3: true } | |
1073 | // all = { cl1: true, cl2: true, cl3: true } | |
1074 | // | |
1075 | // This function returns false if nothing is required. | |
1076 | function extractRequired( required, all ) { | |
1077 | var unbang = [], | |
1078 | empty = true, | |
1079 | i; | |
1080 | ||
1081 | if ( required ) | |
1082 | empty = false; | |
1083 | else | |
1084 | required = {}; | |
1085 | ||
1086 | for ( i in all ) { | |
1087 | if ( i.charAt( 0 ) == '!' ) { | |
1088 | i = i.slice( 1 ); | |
1089 | unbang.push( i ); | |
1090 | required[ i ] = true; | |
1091 | empty = false; | |
1092 | } | |
1093 | } | |
1094 | ||
1095 | while ( ( i = unbang.pop() ) ) { | |
1096 | all[ i ] = all[ '!' + i ]; | |
1097 | delete all[ '!' + i ]; | |
1098 | } | |
1099 | ||
1100 | return empty ? false : required; | |
1101 | } | |
1102 | ||
1103 | // Does the actual filtering by appling allowed content rules | |
1104 | // to the element. | |
1105 | // | |
1106 | // @param {CKEDITOR.filter} that The context. | |
1107 | // @param {CKEDITOR.htmlParser.element} element | |
1108 | // @param {Object} opts The same as in processElement. | |
1109 | function filterElement( that, element, opts ) { | |
1110 | var name = element.name, | |
1111 | privObj = that._, | |
1112 | allowedRules = privObj.allowedRules.elements[ name ], | |
1113 | genericAllowedRules = privObj.allowedRules.generic, | |
1114 | disallowedRules = privObj.disallowedRules.elements[ name ], | |
1115 | genericDisallowedRules = privObj.disallowedRules.generic, | |
1116 | skipRequired = opts.skipRequired, | |
1117 | status = { | |
1118 | // Whether any of rules accepted element. | |
1119 | // If not - it will be stripped. | |
1120 | valid: false, | |
1121 | // Objects containing accepted attributes, classes and styles. | |
1122 | validAttributes: {}, | |
1123 | validClasses: {}, | |
1124 | validStyles: {}, | |
1125 | // Whether all are valid. | |
1126 | // If we know that all element's attrs/classes/styles are valid | |
1127 | // we can skip their validation, to improve performance. | |
1128 | allAttributes: false, | |
1129 | allClasses: false, | |
1130 | allStyles: false, | |
1131 | // Whether element had (before applying DACRs) at least one invalid attribute/class/style. | |
1132 | hadInvalidAttribute: false, | |
1133 | hadInvalidClass: false, | |
1134 | hadInvalidStyle: false | |
1135 | }, | |
1136 | i, l; | |
1137 | ||
1138 | // Early return - if there are no rules for this element (specific or generic), remove it. | |
1139 | if ( !allowedRules && !genericAllowedRules ) | |
1140 | return null; | |
1141 | ||
1142 | // Could not be done yet if there were no transformations and if this | |
1143 | // is real (not mocked) object. | |
1144 | populateProperties( element ); | |
1145 | ||
1146 | // Note - this step modifies element's styles, classes and attributes. | |
1147 | if ( disallowedRules ) { | |
1148 | for ( i = 0, l = disallowedRules.length; i < l; ++i ) { | |
1149 | // Apply rule and make an early return if false is returned what means | |
1150 | // that element is completely disallowed. | |
1151 | if ( applyDisallowedRule( disallowedRules[ i ], element, status ) === false ) | |
1152 | return null; | |
1153 | } | |
1154 | } | |
1155 | ||
1156 | // Note - this step modifies element's styles, classes and attributes. | |
1157 | if ( genericDisallowedRules ) { | |
1158 | for ( i = 0, l = genericDisallowedRules.length; i < l; ++i ) | |
1159 | applyDisallowedRule( genericDisallowedRules[ i ], element, status ); | |
1160 | } | |
1161 | ||
1162 | if ( allowedRules ) { | |
1163 | for ( i = 0, l = allowedRules.length; i < l; ++i ) | |
1164 | applyAllowedRule( allowedRules[ i ], element, status, skipRequired ); | |
1165 | } | |
1166 | ||
1167 | if ( genericAllowedRules ) { | |
1168 | for ( i = 0, l = genericAllowedRules.length; i < l; ++i ) | |
1169 | applyAllowedRule( genericAllowedRules[ i ], element, status, skipRequired ); | |
1170 | } | |
1171 | ||
1172 | return status; | |
1173 | } | |
1174 | ||
1175 | // Check whether element has all properties (styles,classes,attrs) required by a rule. | |
1176 | function hasAllRequired( rule, element ) { | |
1177 | if ( rule.nothingRequired ) | |
1178 | return true; | |
1179 | ||
1180 | var i, req, reqs, existing; | |
1181 | ||
1182 | if ( ( reqs = rule.requiredClasses ) ) { | |
1183 | existing = element.classes; | |
1184 | for ( i = 0; i < reqs.length; ++i ) { | |
1185 | req = reqs[ i ]; | |
1186 | if ( typeof req == 'string' ) { | |
1187 | if ( CKEDITOR.tools.indexOf( existing, req ) == -1 ) | |
1188 | return false; | |
1189 | } | |
1190 | // This means regexp. | |
1191 | else { | |
1192 | if ( !CKEDITOR.tools.checkIfAnyArrayItemMatches( existing, req ) ) | |
1193 | return false; | |
1194 | } | |
1195 | } | |
1196 | } | |
1197 | ||
1198 | return hasAllRequiredInHash( element.styles, rule.requiredStyles ) && | |
1199 | hasAllRequiredInHash( element.attributes, rule.requiredAttributes ); | |
1200 | } | |
1201 | ||
1202 | // Check whether all items in required (array) exist in existing (object). | |
1203 | function hasAllRequiredInHash( existing, required ) { | |
1204 | if ( !required ) | |
1205 | return true; | |
1206 | ||
1207 | for ( var i = 0, req; i < required.length; ++i ) { | |
1208 | req = required[ i ]; | |
1209 | if ( typeof req == 'string' ) { | |
1210 | if ( !( req in existing ) ) | |
1211 | return false; | |
1212 | } | |
1213 | // This means regexp. | |
1214 | else { | |
1215 | if ( !CKEDITOR.tools.checkIfAnyObjectPropertyMatches( existing, req ) ) | |
1216 | return false; | |
1217 | } | |
1218 | } | |
1219 | ||
1220 | return true; | |
1221 | } | |
1222 | ||
1223 | // Create pseudo element that will be passed through filter | |
1224 | // to check if tested string is allowed. | |
1225 | function mockElementFromString( str ) { | |
1226 | var element = parseRulesString( str ).$1, | |
1227 | styles = element.styles, | |
1228 | classes = element.classes; | |
1229 | ||
1230 | element.name = element.elements; | |
1231 | element.classes = classes = ( classes ? classes.split( /\s*,\s*/ ) : [] ); | |
1232 | element.styles = mockHash( styles ); | |
1233 | element.attributes = mockHash( element.attributes ); | |
1234 | element.children = []; | |
1235 | ||
1236 | if ( classes.length ) | |
1237 | element.attributes[ 'class' ] = classes.join( ' ' ); | |
1238 | if ( styles ) | |
1239 | element.attributes.style = CKEDITOR.tools.writeCssText( element.styles ); | |
1240 | ||
1241 | return element; | |
1242 | } | |
1243 | ||
1244 | // Create pseudo element that will be passed through filter | |
1245 | // to check if tested style is allowed. | |
1246 | function mockElementFromStyle( style ) { | |
1247 | var styleDef = style.getDefinition(), | |
1248 | styles = styleDef.styles, | |
1249 | attrs = styleDef.attributes || {}; | |
1250 | ||
1251 | if ( styles ) { | |
1252 | styles = copy( styles ); | |
1253 | attrs.style = CKEDITOR.tools.writeCssText( styles, true ); | |
1254 | } else { | |
1255 | styles = {}; | |
1256 | } | |
1257 | ||
1258 | var el = { | |
1259 | name: styleDef.element, | |
1260 | attributes: attrs, | |
1261 | classes: attrs[ 'class' ] ? attrs[ 'class' ].split( /\s+/ ) : [], | |
1262 | styles: styles, | |
1263 | children: [] | |
1264 | }; | |
1265 | ||
1266 | return el; | |
1267 | } | |
1268 | ||
1269 | // Mock hash based on string. | |
1270 | // 'a,b,c' => { a: 'cke-test', b: 'cke-test', c: 'cke-test' } | |
1271 | // Used to mock styles and attributes objects. | |
1272 | function mockHash( str ) { | |
1273 | // It may be a null or empty string. | |
1274 | if ( !str ) | |
1275 | return {}; | |
1276 | ||
1277 | var keys = str.split( /\s*,\s*/ ).sort(), | |
1278 | obj = {}; | |
1279 | ||
1280 | while ( keys.length ) | |
1281 | obj[ keys.shift() ] = TEST_VALUE; | |
1282 | ||
1283 | return obj; | |
1284 | } | |
1285 | ||
1286 | // Extract properties names from the object | |
1287 | // and replace those containing wildcards with regexps. | |
1288 | // Note: there's a room for performance improvement. Array of mixed types | |
1289 | // breaks JIT-compiler optiomization what may invalidate compilation of pretty a lot of code. | |
1290 | // | |
1291 | // @returns An array of strings and regexps. | |
1292 | function optimizeRequiredProperties( requiredProperties ) { | |
1293 | var arr = []; | |
1294 | for ( var propertyName in requiredProperties ) { | |
1295 | if ( propertyName.indexOf( '*' ) > -1 ) | |
1296 | arr.push( new RegExp( '^' + propertyName.replace( /\*/g, '.*' ) + '$' ) ); | |
1297 | else | |
1298 | arr.push( propertyName ); | |
1299 | } | |
1300 | return arr; | |
1301 | } | |
1302 | ||
1303 | var validators = { styles: 1, attributes: 1, classes: 1 }, | |
1304 | validatorsRequired = { | |
1305 | styles: 'requiredStyles', | |
1306 | attributes: 'requiredAttributes', | |
1307 | classes: 'requiredClasses' | |
1308 | }; | |
1309 | ||
1310 | // Optimize a rule by replacing validators with functions | |
1311 | // and rewriting requiredXXX validators to arrays. | |
1312 | function optimizeRule( rule ) { | |
1313 | var validatorName, | |
1314 | requiredProperties, | |
1315 | i; | |
1316 | ||
1317 | for ( validatorName in validators ) | |
1318 | rule[ validatorName ] = validatorFunction( rule[ validatorName ] ); | |
1319 | ||
1320 | var nothingRequired = true; | |
1321 | for ( i in validatorsRequired ) { | |
1322 | validatorName = validatorsRequired[ i ]; | |
1323 | requiredProperties = optimizeRequiredProperties( rule[ validatorName ] ); | |
1324 | // Don't set anything if there are no required properties. This will allow to | |
1325 | // save some memory by GCing all empty arrays (requiredProperties). | |
1326 | if ( requiredProperties.length ) { | |
1327 | rule[ validatorName ] = requiredProperties; | |
1328 | nothingRequired = false; | |
1329 | } | |
1330 | } | |
1331 | ||
1332 | rule.nothingRequired = nothingRequired; | |
1333 | rule.noProperties = !( rule.attributes || rule.classes || rule.styles ); | |
1334 | } | |
1335 | ||
1336 | // Add optimized version of rule to optimizedRules object. | |
1337 | function optimizeRules( optimizedRules, rules ) { | |
1338 | var elementsRules = optimizedRules.elements, | |
1339 | genericRules = optimizedRules.generic, | |
1340 | i, l, rule, element, priority; | |
1341 | ||
1342 | for ( i = 0, l = rules.length; i < l; ++i ) { | |
1343 | // Shallow copy. Do not modify original rule. | |
1344 | rule = copy( rules[ i ] ); | |
1345 | priority = rule.classes === true || rule.styles === true || rule.attributes === true; | |
1346 | optimizeRule( rule ); | |
1347 | ||
1348 | // E.g. "*(xxx)[xxx]" - it's a generic rule that | |
1349 | // validates properties only. | |
1350 | // Or '$1': { match: function() {...} } | |
1351 | if ( rule.elements === true || rule.elements === null ) { | |
1352 | // Add priority rules at the beginning. | |
1353 | genericRules[ priority ? 'unshift' : 'push' ]( rule ); | |
1354 | } | |
1355 | // If elements list was explicitly defined, | |
1356 | // add this rule for every defined element. | |
1357 | else { | |
1358 | // We don't need elements validator for this kind of rule. | |
1359 | var elements = rule.elements; | |
1360 | delete rule.elements; | |
1361 | ||
1362 | for ( element in elements ) { | |
1363 | if ( !elementsRules[ element ] ) | |
1364 | elementsRules[ element ] = [ rule ]; | |
1365 | else | |
1366 | elementsRules[ element ][ priority ? 'unshift' : 'push' ]( rule ); | |
1367 | } | |
1368 | } | |
1369 | } | |
1370 | } | |
1371 | ||
1372 | // < elements >< styles, attributes and classes >< separator > | |
1373 | var rulePattern = /^([a-z0-9\-*\s]+)((?:\s*\{[!\w\-,\s\*]+\}\s*|\s*\[[!\w\-,\s\*]+\]\s*|\s*\([!\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i, | |
1374 | groupsPatterns = { | |
1375 | styles: /{([^}]+)}/, | |
1376 | attrs: /\[([^\]]+)\]/, | |
1377 | classes: /\(([^\)]+)\)/ | |
1378 | }; | |
1379 | ||
1380 | function parseRulesString( input ) { | |
1381 | var match, | |
1382 | props, styles, attrs, classes, | |
1383 | rules = {}, | |
1384 | groupNum = 1; | |
1385 | ||
1386 | input = trim( input ); | |
1387 | ||
1388 | while ( ( match = input.match( rulePattern ) ) ) { | |
1389 | if ( ( props = match[ 2 ] ) ) { | |
1390 | styles = parseProperties( props, 'styles' ); | |
1391 | attrs = parseProperties( props, 'attrs' ); | |
1392 | classes = parseProperties( props, 'classes' ); | |
1393 | } else { | |
1394 | styles = attrs = classes = null; | |
1395 | } | |
1396 | ||
1397 | // Add as an unnamed rule, because there can be two rules | |
1398 | // for one elements set defined in string format. | |
1399 | rules[ '$' + groupNum++ ] = { | |
1400 | elements: match[ 1 ], | |
1401 | classes: classes, | |
1402 | styles: styles, | |
1403 | attributes: attrs | |
1404 | }; | |
1405 | ||
1406 | // Move to the next group. | |
1407 | input = input.slice( match[ 0 ].length ); | |
1408 | } | |
1409 | ||
1410 | return rules; | |
1411 | } | |
1412 | ||
1413 | // Extract specified properties group (styles, attrs, classes) from | |
1414 | // what stands after the elements list in string format of allowedContent. | |
1415 | function parseProperties( properties, groupName ) { | |
1416 | var group = properties.match( groupsPatterns[ groupName ] ); | |
1417 | return group ? trim( group[ 1 ] ) : null; | |
1418 | } | |
1419 | ||
1420 | function populateProperties( element ) { | |
1421 | // Backup styles and classes, because they may be removed by DACRs. | |
1422 | // We'll need them in updateElement(). | |
1423 | var styles = element.styleBackup = element.attributes.style, | |
1424 | classes = element.classBackup = element.attributes[ 'class' ]; | |
1425 | ||
1426 | // Parse classes and styles if that hasn't been done before. | |
1427 | if ( !element.styles ) | |
1428 | element.styles = CKEDITOR.tools.parseCssText( styles || '', 1 ); | |
1429 | if ( !element.classes ) | |
1430 | element.classes = classes ? classes.split( /\s+/ ) : []; | |
1431 | } | |
1432 | ||
1433 | // Filter element protected with a comment. | |
1434 | // Returns true if protected content is ok, false otherwise. | |
1435 | function processProtectedElement( that, comment, protectedRegexs, filterOpts ) { | |
1436 | var source = decodeURIComponent( comment.value.replace( /^\{cke_protected\}/, '' ) ), | |
1437 | protectedFrag, | |
1438 | toBeRemoved = [], | |
1439 | node, i, match; | |
1440 | ||
1441 | // Protected element's and protected source's comments look exactly the same. | |
1442 | // Check if what we have isn't a protected source instead of protected script/noscript. | |
1443 | if ( protectedRegexs ) { | |
1444 | for ( i = 0; i < protectedRegexs.length; ++i ) { | |
1445 | if ( ( match = source.match( protectedRegexs[ i ] ) ) && | |
1446 | match[ 0 ].length == source.length // Check whether this pattern matches entire source | |
1447 | // to avoid '<script>alert("<? 1 ?>")</script>' matching | |
1448 | // the PHP's protectedSource regexp. | |
1449 | ) | |
1450 | return true; | |
1451 | } | |
1452 | } | |
1453 | ||
1454 | protectedFrag = CKEDITOR.htmlParser.fragment.fromHtml( source ); | |
1455 | ||
1456 | if ( protectedFrag.children.length == 1 && ( node = protectedFrag.children[ 0 ] ).type == CKEDITOR.NODE_ELEMENT ) | |
1457 | processElement( that, node, toBeRemoved, filterOpts ); | |
1458 | ||
1459 | // If protected element has been marked to be removed, return 'false' - comment was rejected. | |
1460 | return !toBeRemoved.length; | |
1461 | } | |
1462 | ||
1463 | var unprotectElementsNamesRegexp = /^cke:(object|embed|param)$/, | |
1464 | protectElementsNamesRegexp = /^(object|embed|param)$/; | |
1465 | ||
1466 | // The actual function which filters, transforms and does other funny things with an element. | |
1467 | // | |
1468 | // @param {CKEDITOR.filter} that Context. | |
1469 | // @param {CKEDITOR.htmlParser.element} element The element to be processed. | |
1470 | // @param {Array} toBeRemoved Array into which elements rejected by the filter will be pushed. | |
1471 | // @param {Boolean} [opts.doFilter] Whether element should be filtered. | |
1472 | // @param {Boolean} [opts.doTransform] Whether transformations should be applied. | |
1473 | // @param {Boolean} [opts.doCallbacks] Whether to execute element callbacks. | |
1474 | // @param {Boolean} [opts.toHtml] Set to true if filter used together with htmlDP#toHtml | |
1475 | // @param {Boolean} [opts.skipRequired] Whether element's required properties shouldn't be verified. | |
1476 | // @param {Boolean} [opts.skipFinalValidation] Whether to not perform final element validation (a,img). | |
1477 | // @returns {Number} Possible flags: | |
1478 | // * FILTER_ELEMENT_MODIFIED, | |
1479 | // * FILTER_SKIP_TREE. | |
1480 | function processElement( that, element, toBeRemoved, opts ) { | |
1481 | var status, | |
1482 | retVal = 0, | |
1483 | callbacksRetVal; | |
1484 | ||
1485 | // Unprotect elements names previously protected by htmlDataProcessor | |
1486 | // (see protectElementNames and protectSelfClosingElements functions). | |
1487 | // Note: body, title, etc. are not protected by htmlDataP (or are protected and then unprotected). | |
1488 | if ( opts.toHtml ) | |
1489 | element.name = element.name.replace( unprotectElementsNamesRegexp, '$1' ); | |
1490 | ||
1491 | // Execute element callbacks and return if one of them returned any value. | |
1492 | if ( opts.doCallbacks && that.elementCallbacks ) { | |
1493 | // For now we only support here FILTER_SKIP_TREE, so we can early return if retVal is truly value. | |
1494 | if ( ( callbacksRetVal = executeElementCallbacks( element, that.elementCallbacks ) ) ) | |
1495 | return callbacksRetVal; | |
1496 | } | |
1497 | ||
1498 | // If transformations are set apply all groups. | |
1499 | if ( opts.doTransform ) | |
1500 | transformElement( that, element ); | |
1501 | ||
1502 | if ( opts.doFilter ) { | |
1503 | // Apply all filters. | |
1504 | status = filterElement( that, element, opts ); | |
1505 | ||
1506 | // Handle early return from filterElement. | |
1507 | if ( !status ) { | |
1508 | toBeRemoved.push( element ); | |
1509 | return FILTER_ELEMENT_MODIFIED; | |
1510 | } | |
1511 | ||
1512 | // Finally, if after running all filter rules it still hasn't been allowed - remove it. | |
1513 | if ( !status.valid ) { | |
1514 | toBeRemoved.push( element ); | |
1515 | return FILTER_ELEMENT_MODIFIED; | |
1516 | } | |
1517 | ||
1518 | // Update element's attributes based on status of filtering. | |
1519 | if ( updateElement( element, status ) ) | |
1520 | retVal = FILTER_ELEMENT_MODIFIED; | |
1521 | ||
1522 | if ( !opts.skipFinalValidation && !validateElement( element ) ) { | |
1523 | toBeRemoved.push( element ); | |
1524 | return FILTER_ELEMENT_MODIFIED; | |
1525 | } | |
1526 | } | |
1527 | ||
1528 | // Protect previously unprotected elements. | |
1529 | if ( opts.toHtml ) | |
1530 | element.name = element.name.replace( protectElementsNamesRegexp, 'cke:$1' ); | |
1531 | ||
1532 | return retVal; | |
1533 | } | |
1534 | ||
1535 | // Returns a regexp object which can be used to test if a property | |
1536 | // matches one of wildcard validators. | |
1537 | function regexifyPropertiesWithWildcards( validators ) { | |
1538 | var patterns = [], | |
1539 | i; | |
1540 | ||
1541 | for ( i in validators ) { | |
1542 | if ( i.indexOf( '*' ) > -1 ) | |
1543 | patterns.push( i.replace( /\*/g, '.*' ) ); | |
1544 | } | |
1545 | ||
1546 | if ( patterns.length ) | |
1547 | return new RegExp( '^(?:' + patterns.join( '|' ) + ')$' ); | |
1548 | else | |
1549 | return null; | |
1550 | } | |
1551 | ||
1552 | // Standardize a rule by converting all validators to hashes. | |
1553 | function standardizeRule( rule ) { | |
1554 | rule.elements = convertValidatorToHash( rule.elements, /\s+/ ) || null; | |
1555 | rule.propertiesOnly = rule.propertiesOnly || ( rule.elements === true ); | |
1556 | ||
1557 | var delim = /\s*,\s*/, | |
1558 | i; | |
1559 | ||
1560 | for ( i in validators ) { | |
1561 | rule[ i ] = convertValidatorToHash( rule[ i ], delim ) || null; | |
1562 | rule[ validatorsRequired[ i ] ] = extractRequired( convertValidatorToHash( | |
1563 | rule[ validatorsRequired[ i ] ], delim ), rule[ i ] ) || null; | |
1564 | } | |
1565 | ||
1566 | rule.match = rule.match || null; | |
1567 | } | |
1568 | ||
1569 | // Does the element transformation by applying registered | |
1570 | // transformation rules. | |
1571 | function transformElement( that, element ) { | |
1572 | var transformations = that._.transformations[ element.name ], | |
1573 | i; | |
1574 | ||
1575 | if ( !transformations ) | |
1576 | return; | |
1577 | ||
1578 | populateProperties( element ); | |
1579 | ||
1580 | for ( i = 0; i < transformations.length; ++i ) | |
1581 | applyTransformationsGroup( that, element, transformations[ i ] ); | |
1582 | ||
1583 | // Do not count on updateElement() which is called in processElement, because it: | |
1584 | // * may not be called, | |
1585 | // * may skip some properties when all are marked as valid. | |
1586 | updateAttributes( element ); | |
1587 | } | |
1588 | ||
1589 | // Copy element's styles and classes back to attributes array. | |
1590 | function updateAttributes( element ) { | |
1591 | var attrs = element.attributes, | |
1592 | styles; | |
1593 | ||
1594 | // Will be recreated later if any of styles/classes exists. | |
1595 | delete attrs.style; | |
1596 | delete attrs[ 'class' ]; | |
1597 | ||
1598 | if ( ( styles = CKEDITOR.tools.writeCssText( element.styles, true ) ) ) | |
1599 | attrs.style = styles; | |
1600 | ||
1601 | if ( element.classes.length ) | |
1602 | attrs[ 'class' ] = element.classes.sort().join( ' ' ); | |
1603 | } | |
1604 | ||
1605 | // Update element object based on status of filtering. | |
1606 | // @returns Whether element was modified. | |
1607 | function updateElement( element, status ) { | |
1608 | var validAttrs = status.validAttributes, | |
1609 | validStyles = status.validStyles, | |
1610 | validClasses = status.validClasses, | |
1611 | attrs = element.attributes, | |
1612 | styles = element.styles, | |
1613 | classes = element.classes, | |
1614 | origClasses = element.classBackup, | |
1615 | origStyles = element.styleBackup, | |
1616 | name, origName, i, | |
1617 | stylesArr = [], | |
1618 | classesArr = [], | |
1619 | internalAttr = /^data-cke-/, | |
1620 | isModified = false; | |
1621 | ||
1622 | // Will be recreated later if any of styles/classes were passed. | |
1623 | delete attrs.style; | |
1624 | delete attrs[ 'class' ]; | |
1625 | // Clean up. | |
1626 | delete element.classBackup; | |
1627 | delete element.styleBackup; | |
1628 | ||
1629 | if ( !status.allAttributes ) { | |
1630 | for ( name in attrs ) { | |
1631 | // If not valid and not internal attribute delete it. | |
1632 | if ( !validAttrs[ name ] ) { | |
1633 | // Allow all internal attibutes... | |
1634 | if ( internalAttr.test( name ) ) { | |
1635 | // ... unless this is a saved attribute and the original one isn't allowed. | |
1636 | if ( name != ( origName = name.replace( /^data-cke-saved-/, '' ) ) && | |
1637 | !validAttrs[ origName ] | |
1638 | ) { | |
1639 | delete attrs[ name ]; | |
1640 | isModified = true; | |
1641 | } | |
1642 | } else { | |
1643 | delete attrs[ name ]; | |
1644 | isModified = true; | |
1645 | } | |
1646 | } | |
1647 | ||
1648 | } | |
1649 | } | |
1650 | ||
1651 | if ( !status.allStyles || status.hadInvalidStyle ) { | |
1652 | for ( name in styles ) { | |
1653 | // We check status.allStyles because when there was a '*' ACR and some | |
1654 | // DACR we have now both properties true - status.allStyles and status.hadInvalidStyle. | |
1655 | // However unlike in the case when we only have '*' ACR, in which we can just copy original | |
1656 | // styles, in this case we must copy only those styles which were not removed by DACRs. | |
1657 | if ( status.allStyles || validStyles[ name ] ) | |
1658 | stylesArr.push( name + ':' + styles[ name ] ); | |
1659 | else | |
1660 | isModified = true; | |
1661 | } | |
1662 | if ( stylesArr.length ) | |
1663 | attrs.style = stylesArr.sort().join( '; ' ); | |
1664 | } | |
1665 | else if ( origStyles ) { | |
1666 | attrs.style = origStyles; | |
1667 | } | |
1668 | ||
1669 | if ( !status.allClasses || status.hadInvalidClass ) { | |
1670 | for ( i = 0; i < classes.length; ++i ) { | |
1671 | // See comment for styles. | |
1672 | if ( status.allClasses || validClasses[ classes[ i ] ] ) | |
1673 | classesArr.push( classes[ i ] ); | |
1674 | } | |
1675 | if ( classesArr.length ) | |
1676 | attrs[ 'class' ] = classesArr.sort().join( ' ' ); | |
1677 | ||
1678 | if ( origClasses && classesArr.length < origClasses.split( /\s+/ ).length ) | |
1679 | isModified = true; | |
1680 | } | |
1681 | else if ( origClasses ) { | |
1682 | attrs[ 'class' ] = origClasses; | |
1683 | } | |
1684 | ||
1685 | return isModified; | |
1686 | } | |
1687 | ||
1688 | function validateElement( element ) { | |
1689 | switch ( element.name ) { | |
1690 | case 'a': | |
1691 | // Code borrowed from htmlDataProcessor, so ACF does the same clean up. | |
1692 | if ( !( element.children.length || element.attributes.name || element.attributes.id ) ) | |
1693 | return false; | |
1694 | break; | |
1695 | case 'img': | |
1696 | if ( !element.attributes.src ) | |
1697 | return false; | |
1698 | break; | |
1699 | } | |
1700 | ||
1701 | return true; | |
1702 | } | |
1703 | ||
1704 | function validatorFunction( validator ) { | |
1705 | if ( !validator ) | |
1706 | return false; | |
1707 | if ( validator === true ) | |
1708 | return true; | |
1709 | ||
1710 | // Note: We don't need to remove properties with wildcards from the validator object. | |
1711 | // E.g. data-* is actually an edge case of /^data-.*$/, so when it's accepted | |
1712 | // by `value in validator` it's ok. | |
1713 | var regexp = regexifyPropertiesWithWildcards( validator ); | |
1714 | ||
1715 | return function( value ) { | |
1716 | return value in validator || ( regexp && value.match( regexp ) ); | |
1717 | }; | |
1718 | } | |
1719 | ||
1720 | // | |
1721 | // REMOVE ELEMENT --------------------------------------------------------- | |
1722 | // | |
1723 | ||
1724 | // Check whether all children will be valid in new context. | |
1725 | // Note: it doesn't verify if text node is valid, because | |
1726 | // new parent should accept them. | |
1727 | function checkChildren( children, newParentName ) { | |
1728 | var allowed = DTD[ newParentName ]; | |
1729 | ||
1730 | for ( var i = 0, l = children.length, child; i < l; ++i ) { | |
1731 | child = children[ i ]; | |
1732 | if ( child.type == CKEDITOR.NODE_ELEMENT && !allowed[ child.name ] ) | |
1733 | return false; | |
1734 | } | |
1735 | ||
1736 | return true; | |
1737 | } | |
1738 | ||
1739 | function createBr() { | |
1740 | return new CKEDITOR.htmlParser.element( 'br' ); | |
1741 | } | |
1742 | ||
1743 | // Whether this is an inline element or text. | |
1744 | function inlineNode( node ) { | |
1745 | return node.type == CKEDITOR.NODE_TEXT || | |
1746 | node.type == CKEDITOR.NODE_ELEMENT && DTD.$inline[ node.name ]; | |
1747 | } | |
1748 | ||
1749 | function isBrOrBlock( node ) { | |
1750 | return node.type == CKEDITOR.NODE_ELEMENT && | |
1751 | ( node.name == 'br' || DTD.$block[ node.name ] ); | |
1752 | } | |
1753 | ||
1754 | // Try to remove element in the best possible way. | |
1755 | // | |
1756 | // @param {Array} toBeChecked After executing this function | |
1757 | // this array will contain elements that should be checked | |
1758 | // because they were marked as potentially: | |
1759 | // * in wrong context (e.g. li in body), | |
1760 | // * empty elements from $removeEmpty, | |
1761 | // * incorrect img/a/other element validated by validateElement(). | |
1762 | function removeElement( element, enterTag, toBeChecked ) { | |
1763 | var name = element.name; | |
1764 | ||
1765 | if ( DTD.$empty[ name ] || !element.children.length ) { | |
1766 | // Special case - hr in br mode should be replaced with br, not removed. | |
1767 | if ( name == 'hr' && enterTag == 'br' ) | |
1768 | element.replaceWith( createBr() ); | |
1769 | else { | |
1770 | // Parent might become an empty inline specified in $removeEmpty or empty a[href]. | |
1771 | if ( element.parent ) | |
1772 | toBeChecked.push( { check: 'it', el: element.parent } ); | |
1773 | ||
1774 | element.remove(); | |
1775 | } | |
1776 | } else if ( DTD.$block[ name ] || name == 'tr' ) { | |
1777 | if ( enterTag == 'br' ) | |
1778 | stripBlockBr( element, toBeChecked ); | |
1779 | else | |
1780 | stripBlock( element, enterTag, toBeChecked ); | |
1781 | } | |
1782 | // Special case - elements that may contain CDATA should be removed completely. | |
1783 | else if ( name in { style: 1, script: 1 } ) | |
1784 | element.remove(); | |
1785 | // The rest of inline elements. May also be the last resort | |
1786 | // for some special elements. | |
1787 | else { | |
1788 | // Parent might become an empty inline specified in $removeEmpty or empty a[href]. | |
1789 | if ( element.parent ) | |
1790 | toBeChecked.push( { check: 'it', el: element.parent } ); | |
1791 | element.replaceWithChildren(); | |
1792 | } | |
1793 | } | |
1794 | ||
1795 | // Strip element block, but leave its content. | |
1796 | // Works in 'div' and 'p' enter modes. | |
1797 | function stripBlock( element, enterTag, toBeChecked ) { | |
1798 | var children = element.children; | |
1799 | ||
1800 | // First, check if element's children may be wrapped with <p/div>. | |
1801 | // Ignore that <p/div> may not be allowed in element.parent. | |
1802 | // This will be fixed when removing parent or by toBeChecked rule. | |
1803 | if ( checkChildren( children, enterTag ) ) { | |
1804 | element.name = enterTag; | |
1805 | element.attributes = {}; | |
1806 | // Check if this p/div was put in correct context. | |
1807 | // If not - strip parent. | |
1808 | toBeChecked.push( { check: 'parent-down', el: element } ); | |
1809 | return; | |
1810 | } | |
1811 | ||
1812 | var parent = element.parent, | |
1813 | shouldAutoP = parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT || parent.name == 'body', | |
1814 | i, child, p, parentDtd; | |
1815 | ||
1816 | for ( i = children.length; i > 0; ) { | |
1817 | child = children[ --i ]; | |
1818 | ||
1819 | // If parent requires auto paragraphing and child is inline node, | |
1820 | // insert this child into newly created paragraph. | |
1821 | if ( shouldAutoP && inlineNode( child ) ) { | |
1822 | if ( !p ) { | |
1823 | p = new CKEDITOR.htmlParser.element( enterTag ); | |
1824 | p.insertAfter( element ); | |
1825 | ||
1826 | // Check if this p/div was put in correct context. | |
1827 | // If not - strip parent. | |
1828 | toBeChecked.push( { check: 'parent-down', el: p } ); | |
1829 | } | |
1830 | p.add( child, 0 ); | |
1831 | } | |
1832 | // Child which doesn't need to be auto paragraphed. | |
1833 | else { | |
1834 | p = null; | |
1835 | parentDtd = DTD[ parent.name ] || DTD.span; | |
1836 | ||
1837 | child.insertAfter( element ); | |
1838 | // If inserted into invalid context, mark it and check | |
1839 | // after removing all elements. | |
1840 | if ( parent.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT && | |
1841 | child.type == CKEDITOR.NODE_ELEMENT && | |
1842 | !parentDtd[ child.name ] | |
1843 | ) | |
1844 | toBeChecked.push( { check: 'el-up', el: child } ); | |
1845 | } | |
1846 | } | |
1847 | ||
1848 | // All children have been moved to element's parent, so remove it. | |
1849 | element.remove(); | |
1850 | } | |
1851 | ||
1852 | // Prepend/append block with <br> if isn't | |
1853 | // already prepended/appended with <br> or block and | |
1854 | // isn't first/last child of its parent. | |
1855 | // Then replace element with its children. | |
1856 | // <p>a</p><p>b</p> => <p>a</p><br>b => a<br>b | |
1857 | function stripBlockBr( element ) { | |
1858 | var br; | |
1859 | ||
1860 | if ( element.previous && !isBrOrBlock( element.previous ) ) { | |
1861 | br = createBr(); | |
1862 | br.insertBefore( element ); | |
1863 | } | |
1864 | ||
1865 | if ( element.next && !isBrOrBlock( element.next ) ) { | |
1866 | br = createBr(); | |
1867 | br.insertAfter( element ); | |
1868 | } | |
1869 | ||
1870 | element.replaceWithChildren(); | |
1871 | } | |
1872 | ||
1873 | // | |
1874 | // TRANSFORMATIONS -------------------------------------------------------- | |
1875 | // | |
1876 | ||
1877 | // Apply given transformations group to the element. | |
1878 | function applyTransformationsGroup( filter, element, group ) { | |
1879 | var i, rule; | |
1880 | ||
1881 | for ( i = 0; i < group.length; ++i ) { | |
1882 | rule = group[ i ]; | |
1883 | ||
1884 | // Test with #check or #left only if it's set. | |
1885 | // Do not apply transformations because that creates infinite loop. | |
1886 | if ( ( !rule.check || filter.check( rule.check, false ) ) && | |
1887 | ( !rule.left || rule.left( element ) ) ) { | |
1888 | rule.right( element, transformationsTools ); | |
1889 | return; // Only first matching rule in a group is executed. | |
1890 | } | |
1891 | } | |
1892 | } | |
1893 | ||
1894 | // Check whether element matches CKEDITOR.style. | |
1895 | // The element can be a "superset" of style, | |
1896 | // e.g. it may have more classes, but need to have | |
1897 | // at least those defined in style. | |
1898 | function elementMatchesStyle( element, style ) { | |
1899 | var def = style.getDefinition(), | |
1900 | defAttrs = def.attributes, | |
1901 | defStyles = def.styles, | |
1902 | attrName, styleName, | |
1903 | classes, classPattern, cl; | |
1904 | ||
1905 | if ( element.name != def.element ) | |
1906 | return false; | |
1907 | ||
1908 | for ( attrName in defAttrs ) { | |
1909 | if ( attrName == 'class' ) { | |
1910 | classes = defAttrs[ attrName ].split( /\s+/ ); | |
1911 | classPattern = element.classes.join( '|' ); | |
1912 | while ( ( cl = classes.pop() ) ) { | |
1913 | if ( classPattern.indexOf( cl ) == -1 ) | |
1914 | return false; | |
1915 | } | |
1916 | } else { | |
1917 | if ( element.attributes[ attrName ] != defAttrs[ attrName ] ) | |
1918 | return false; | |
1919 | } | |
1920 | } | |
1921 | ||
1922 | for ( styleName in defStyles ) { | |
1923 | if ( element.styles[ styleName ] != defStyles[ styleName ] ) | |
1924 | return false; | |
1925 | } | |
1926 | ||
1927 | return true; | |
1928 | } | |
1929 | ||
1930 | // Return transformation group for content form. | |
1931 | // One content form makes one transformation rule in one group. | |
1932 | function getContentFormTransformationGroup( form, preferredForm ) { | |
1933 | var element, left; | |
1934 | ||
1935 | if ( typeof form == 'string' ) | |
1936 | element = form; | |
1937 | else if ( form instanceof CKEDITOR.style ) | |
1938 | left = form; | |
1939 | else { | |
1940 | element = form[ 0 ]; | |
1941 | left = form[ 1 ]; | |
1942 | } | |
1943 | ||
1944 | return [ { | |
1945 | element: element, | |
1946 | left: left, | |
1947 | right: function( el, tools ) { | |
1948 | tools.transform( el, preferredForm ); | |
1949 | } | |
1950 | } ]; | |
1951 | } | |
1952 | ||
1953 | // Obtain element's name from transformation rule. | |
1954 | // It will be defined by #element, or #check or #left (styleDef.element). | |
1955 | function getElementNameForTransformation( rule, check ) { | |
1956 | if ( rule.element ) | |
1957 | return rule.element; | |
1958 | if ( check ) | |
1959 | return check.match( /^([a-z0-9]+)/i )[ 0 ]; | |
1960 | return rule.left.getDefinition().element; | |
1961 | } | |
1962 | ||
1963 | function getMatchStyleFn( style ) { | |
1964 | return function( el ) { | |
1965 | return elementMatchesStyle( el, style ); | |
1966 | }; | |
1967 | } | |
1968 | ||
1969 | function getTransformationFn( toolName ) { | |
1970 | return function( el, tools ) { | |
1971 | tools[ toolName ]( el ); | |
1972 | }; | |
1973 | } | |
1974 | ||
1975 | function optimizeTransformationsGroup( rules ) { | |
1976 | var groupName, i, rule, | |
1977 | check, left, right, | |
1978 | optimizedRules = []; | |
1979 | ||
1980 | for ( i = 0; i < rules.length; ++i ) { | |
1981 | rule = rules[ i ]; | |
1982 | ||
1983 | if ( typeof rule == 'string' ) { | |
1984 | rule = rule.split( /\s*:\s*/ ); | |
1985 | check = rule[ 0 ]; | |
1986 | left = null; | |
1987 | right = rule[ 1 ]; | |
1988 | } else { | |
1989 | check = rule.check; | |
1990 | left = rule.left; | |
1991 | right = rule.right; | |
1992 | } | |
1993 | ||
1994 | // Extract element name. | |
1995 | if ( !groupName ) | |
1996 | groupName = getElementNameForTransformation( rule, check ); | |
1997 | ||
1998 | if ( left instanceof CKEDITOR.style ) | |
1999 | left = getMatchStyleFn( left ); | |
2000 | ||
2001 | optimizedRules.push( { | |
2002 | // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. | |
2003 | check: check == groupName ? null : check, | |
2004 | ||
2005 | left: left, | |
2006 | ||
2007 | // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'. | |
2008 | right: typeof right == 'string' ? getTransformationFn( right ) : right | |
2009 | } ); | |
2010 | } | |
2011 | ||
2012 | return { | |
2013 | name: groupName, | |
2014 | rules: optimizedRules | |
2015 | }; | |
2016 | } | |
2017 | ||
2018 | /** | |
2019 | * Singleton containing tools useful for transformation rules. | |
2020 | * | |
2021 | * @class CKEDITOR.filter.transformationsTools | |
2022 | * @singleton | |
2023 | */ | |
2024 | var transformationsTools = CKEDITOR.filter.transformationsTools = { | |
2025 | /** | |
2026 | * Converts `width` and `height` attributes to styles. | |
2027 | * | |
2028 | * @param {CKEDITOR.htmlParser.element} element | |
2029 | */ | |
2030 | sizeToStyle: function( element ) { | |
2031 | this.lengthToStyle( element, 'width' ); | |
2032 | this.lengthToStyle( element, 'height' ); | |
2033 | }, | |
2034 | ||
2035 | /** | |
2036 | * Converts `width` and `height` styles to attributes. | |
2037 | * | |
2038 | * @param {CKEDITOR.htmlParser.element} element | |
2039 | */ | |
2040 | sizeToAttribute: function( element ) { | |
2041 | this.lengthToAttribute( element, 'width' ); | |
2042 | this.lengthToAttribute( element, 'height' ); | |
2043 | }, | |
2044 | ||
2045 | /** | |
2046 | * Converts length in the `attrName` attribute to a valid CSS length (like `width` or `height`). | |
2047 | * | |
2048 | * @param {CKEDITOR.htmlParser.element} element | |
2049 | * @param {String} attrName Name of the attribute that will be converted. | |
2050 | * @param {String} [styleName=attrName] Name of the style into which the attribute will be converted. | |
2051 | */ | |
2052 | lengthToStyle: function( element, attrName, styleName ) { | |
2053 | styleName = styleName || attrName; | |
2054 | ||
2055 | if ( !( styleName in element.styles ) ) { | |
2056 | var value = element.attributes[ attrName ]; | |
2057 | ||
2058 | if ( value ) { | |
2059 | if ( ( /^\d+$/ ).test( value ) ) | |
2060 | value += 'px'; | |
2061 | ||
2062 | element.styles[ styleName ] = value; | |
2063 | } | |
2064 | } | |
2065 | ||
2066 | delete element.attributes[ attrName ]; | |
2067 | }, | |
2068 | ||
2069 | /** | |
2070 | * Converts length in the `styleName` style to a valid length attribute (like `width` or `height`). | |
2071 | * | |
2072 | * @param {CKEDITOR.htmlParser.element} element | |
2073 | * @param {String} styleName Name of the style that will be converted. | |
2074 | * @param {String} [attrName=styleName] Name of the attribute into which the style will be converted. | |
2075 | */ | |
2076 | lengthToAttribute: function( element, styleName, attrName ) { | |
2077 | attrName = attrName || styleName; | |
2078 | ||
2079 | if ( !( attrName in element.attributes ) ) { | |
2080 | var value = element.styles[ styleName ], | |
2081 | match = value && value.match( /^(\d+)(?:\.\d*)?px$/ ); | |
2082 | ||
2083 | if ( match ) | |
2084 | element.attributes[ attrName ] = match[ 1 ]; | |
2085 | // Pass the TEST_VALUE used by filter#check when mocking element. | |
2086 | else if ( value == TEST_VALUE ) | |
2087 | element.attributes[ attrName ] = TEST_VALUE; | |
2088 | } | |
2089 | ||
2090 | delete element.styles[ styleName ]; | |
2091 | }, | |
2092 | ||
2093 | /** | |
2094 | * Converts the `align` attribute to the `float` style if not set. Attribute | |
2095 | * is always removed. | |
2096 | * | |
2097 | * @param {CKEDITOR.htmlParser.element} element | |
2098 | */ | |
2099 | alignmentToStyle: function( element ) { | |
2100 | if ( !( 'float' in element.styles ) ) { | |
2101 | var value = element.attributes.align; | |
2102 | ||
2103 | if ( value == 'left' || value == 'right' ) | |
2104 | element.styles[ 'float' ] = value; // Uh... GCC doesn't like the 'float' prop name. | |
2105 | } | |
2106 | ||
2107 | delete element.attributes.align; | |
2108 | }, | |
2109 | ||
2110 | /** | |
2111 | * Converts the `float` style to the `align` attribute if not set. | |
2112 | * Style is always removed. | |
2113 | * | |
2114 | * @param {CKEDITOR.htmlParser.element} element | |
2115 | */ | |
2116 | alignmentToAttribute: function( element ) { | |
2117 | if ( !( 'align' in element.attributes ) ) { | |
2118 | var value = element.styles[ 'float' ]; | |
2119 | ||
2120 | if ( value == 'left' || value == 'right' ) | |
2121 | element.attributes.align = value; | |
2122 | } | |
2123 | ||
2124 | delete element.styles[ 'float' ]; // Uh... GCC doesn't like the 'float' prop name. | |
2125 | }, | |
2126 | ||
2127 | /** | |
2128 | * Checks whether an element matches a given {@link CKEDITOR.style}. | |
2129 | * The element can be a "superset" of a style, e.g. it may have | |
2130 | * more classes, but needs to have at least those defined in the style. | |
2131 | * | |
2132 | * @param {CKEDITOR.htmlParser.element} element | |
2133 | * @param {CKEDITOR.style} style | |
2134 | */ | |
2135 | matchesStyle: elementMatchesStyle, | |
2136 | ||
2137 | /** | |
2138 | * Transforms element to given form. | |
2139 | * | |
2140 | * Form may be a: | |
2141 | * | |
2142 | * * {@link CKEDITOR.style}, | |
2143 | * * string – the new name of an element. | |
2144 | * | |
2145 | * @param {CKEDITOR.htmlParser.element} el | |
2146 | * @param {CKEDITOR.style/String} form | |
2147 | */ | |
2148 | transform: function( el, form ) { | |
2149 | if ( typeof form == 'string' ) | |
2150 | el.name = form; | |
2151 | // Form is an instance of CKEDITOR.style. | |
2152 | else { | |
2153 | var def = form.getDefinition(), | |
2154 | defStyles = def.styles, | |
2155 | defAttrs = def.attributes, | |
2156 | attrName, styleName, | |
2157 | existingClassesPattern, defClasses, cl; | |
2158 | ||
2159 | el.name = def.element; | |
2160 | ||
2161 | for ( attrName in defAttrs ) { | |
2162 | if ( attrName == 'class' ) { | |
2163 | existingClassesPattern = el.classes.join( '|' ); | |
2164 | defClasses = defAttrs[ attrName ].split( /\s+/ ); | |
2165 | ||
2166 | while ( ( cl = defClasses.pop() ) ) { | |
2167 | if ( existingClassesPattern.indexOf( cl ) == -1 ) | |
2168 | el.classes.push( cl ); | |
2169 | } | |
2170 | } else { | |
2171 | el.attributes[ attrName ] = defAttrs[ attrName ]; | |
2172 | } | |
2173 | ||
2174 | } | |
2175 | ||
2176 | for ( styleName in defStyles ) { | |
2177 | el.styles[ styleName ] = defStyles[ styleName ]; | |
2178 | } | |
2179 | } | |
2180 | } | |
2181 | }; | |
2182 | ||
2183 | } )(); | |
2184 | ||
2185 | /** | |
2186 | * Allowed content rules. This setting is used when | |
2187 | * instantiating {@link CKEDITOR.editor#filter}. | |
2188 | * | |
2189 | * The following values are accepted: | |
2190 | * | |
2191 | * * {@link CKEDITOR.filter.allowedContentRules} – defined rules will be added | |
2192 | * to the {@link CKEDITOR.editor#filter}. | |
2193 | * * `true` – will disable the filter (data will not be filtered, | |
2194 | * all features will be activated). | |
2195 | * * default – the filter will be configured by loaded features | |
2196 | * (toolbar items, commands, etc.). | |
2197 | * | |
2198 | * In all cases filter configuration may be extended by | |
2199 | * {@link CKEDITOR.config#extraAllowedContent}. This option may be especially | |
2200 | * useful when you want to use the default `allowedContent` value | |
2201 | * along with some additional rules. | |
2202 | * | |
2203 | * CKEDITOR.replace( 'textarea_id', { | |
2204 | * allowedContent: 'p b i; a[!href]', | |
2205 | * on: { | |
2206 | * instanceReady: function( evt ) { | |
2207 | * var editor = evt.editor; | |
2208 | * | |
2209 | * editor.filter.check( 'h1' ); // -> false | |
2210 | * editor.setData( '<h1><i>Foo</i></h1><p class="left"><span>Bar</span> <a href="http://foo.bar">foo</a></p>' ); | |
2211 | * // Editor contents will be: | |
2212 | * '<p><i>Foo</i></p><p>Bar <a href="http://foo.bar">foo</a></p>' | |
2213 | * } | |
2214 | * } | |
2215 | * } ); | |
2216 | * | |
2217 | * It is also possible to disallow some already allowed content. It is especially | |
2218 | * useful when you want to "trim down" the content allowed by default by | |
2219 | * editor features. To do that, use the {@link #disallowedContent} option. | |
2220 | * | |
2221 | * Read more in the [documentation](#!/guide/dev_acf) | |
2222 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | |
2223 | * | |
2224 | * @since 4.1 | |
2225 | * @cfg {CKEDITOR.filter.allowedContentRules/Boolean} [allowedContent=null] | |
2226 | * @member CKEDITOR.config | |
2227 | */ | |
2228 | ||
2229 | /** | |
2230 | * This option makes it possible to set additional allowed | |
2231 | * content rules for {@link CKEDITOR.editor#filter}. | |
2232 | * | |
2233 | * It is especially useful in combination with the default | |
2234 | * {@link CKEDITOR.config#allowedContent} value: | |
2235 | * | |
2236 | * CKEDITOR.replace( 'textarea_id', { | |
2237 | * plugins: 'wysiwygarea,toolbar,format', | |
2238 | * extraAllowedContent: 'b i', | |
2239 | * on: { | |
2240 | * instanceReady: function( evt ) { | |
2241 | * var editor = evt.editor; | |
2242 | * | |
2243 | * editor.filter.check( 'h1' ); // -> true (thanks to Format combo) | |
2244 | * editor.filter.check( 'b' ); // -> true (thanks to extraAllowedContent) | |
2245 | * editor.setData( '<h1><i>Foo</i></h1><p class="left"><b>Bar</b> <a href="http://foo.bar">foo</a></p>' ); | |
2246 | * // Editor contents will be: | |
2247 | * '<h1><i>Foo</i></h1><p><b>Bar</b> foo</p>' | |
2248 | * } | |
2249 | * } | |
2250 | * } ); | |
2251 | * | |
2252 | * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-and-allow-additional-tags%2Fproperties) | |
2253 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | |
2254 | * See also {@link CKEDITOR.config#allowedContent} for more details. | |
2255 | * | |
2256 | * @since 4.1 | |
2257 | * @cfg {Object/String} extraAllowedContent | |
2258 | * @member CKEDITOR.config | |
2259 | */ | |
2260 | ||
2261 | /** | |
2262 | * Disallowed content rules. They have precedence over {@link #allowedContent allowed content rules}. | |
2263 | * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). | |
2264 | * | |
2265 | * Read more in the [documentation](#!/guide/dev_acf-section-automatic-mode-but-disallow-certain-tags%2Fproperties) | |
2266 | * and see the [SDK sample](http://sdk.ckeditor.com/samples/acf.html). | |
2267 | * See also {@link CKEDITOR.config#allowedContent} and {@link CKEDITOR.config#extraAllowedContent}. | |
2268 | * | |
2269 | * @since 4.4 | |
2270 | * @cfg {CKEDITOR.filter.disallowedContentRules} disallowedContent | |
2271 | * @member CKEDITOR.config | |
2272 | */ | |
2273 | ||
2274 | /** | |
2275 | * This event is fired when {@link CKEDITOR.filter} has stripped some | |
2276 | * content from the data that was loaded (e.g. by {@link CKEDITOR.editor#method-setData} | |
2277 | * method or in the source mode) or inserted (e.g. when pasting or using the | |
2278 | * {@link CKEDITOR.editor#method-insertHtml} method). | |
2279 | * | |
2280 | * This event is useful when testing whether the {@link CKEDITOR.config#allowedContent} | |
2281 | * setting is sufficient and correct for a system that is migrating to CKEditor 4.1 | |
2282 | * (where the [Advanced Content Filter](#!/guide/dev_advanced_content_filter) was introduced). | |
2283 | * | |
2284 | * @since 4.1 | |
2285 | * @event dataFiltered | |
2286 | * @member CKEDITOR.editor | |
2287 | * @param {CKEDITOR.editor} editor This editor instance. | |
2288 | */ | |
2289 | ||
2290 | /** | |
2291 | * Virtual class which is the [Allowed Content Rules](#!/guide/dev_allowed_content_rules) formats type. | |
2292 | * | |
2293 | * Possible formats are: | |
2294 | * | |
2295 | * * the [string format](#!/guide/dev_allowed_content_rules-section-2), | |
2296 | * * the [object format](#!/guide/dev_allowed_content_rules-section-3), | |
2297 | * * a {@link CKEDITOR.style} instance – used mainly for integrating plugins with Advanced Content Filter, | |
2298 | * * an array of the above formats. | |
2299 | * | |
2300 | * @since 4.1 | |
2301 | * @class CKEDITOR.filter.allowedContentRules | |
2302 | * @abstract | |
2303 | */ | |
2304 | ||
2305 | /** | |
2306 | * Virtual class representing the {@link CKEDITOR.filter#disallow} argument and a type of | |
2307 | * the {@link CKEDITOR.config#disallowedContent} option. | |
2308 | * | |
2309 | * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. | |
2310 | * Only the string format and object format are accepted. Required properties | |
2311 | * are not allowed in this format. | |
2312 | * | |
2313 | * Read more in the [Disallowed Content guide](#!/guide/dev_disallowed_content). | |
2314 | * | |
2315 | * @since 4.4 | |
2316 | * @class CKEDITOR.filter.disallowedContentRules | |
2317 | * @abstract | |
2318 | */ | |
2319 | ||
2320 | /** | |
2321 | * Virtual class representing {@link CKEDITOR.filter#check} argument. | |
2322 | * | |
2323 | * This is a simplified version of the {@link CKEDITOR.filter.allowedContentRules} type. | |
2324 | * It may contain only one element and its styles, classes, and attributes. Only the | |
2325 | * string format and a {@link CKEDITOR.style} instances are accepted. Required properties | |
2326 | * are not allowed in this format. | |
2327 | * | |
2328 | * Example: | |
2329 | * | |
2330 | * 'img[src,alt](foo)' // Correct rule. | |
2331 | * 'ol, ul(!foo)' // Incorrect rule. Multiple elements and required | |
2332 | * // properties are not supported. | |
2333 | * | |
2334 | * @since 4.1 | |
2335 | * @class CKEDITOR.filter.contentRule | |
2336 | * @abstract | |
2337 | */ | |
2338 | ||
2339 | /** | |
2340 | * Interface that may be automatically implemented by any | |
2341 | * instance of any class which has at least the `name` property and | |
2342 | * can be meant as an editor feature. | |
2343 | * | |
2344 | * For example: | |
2345 | * | |
2346 | * * "Bold" command, button, and keystroke – it does not mean exactly | |
2347 | * `<strong>` or `<b>` but just the ability to create bold text. | |
2348 | * * "Format" drop-down list – it also does not imply any HTML tag. | |
2349 | * * "Link" command, button, and keystroke. | |
2350 | * * "Image" command, button, and dialog window. | |
2351 | * | |
2352 | * Thus most often a feature is an instance of one of the following classes: | |
2353 | * | |
2354 | * * {@link CKEDITOR.command} | |
2355 | * * {@link CKEDITOR.ui.button} | |
2356 | * * {@link CKEDITOR.ui.richCombo} | |
2357 | * | |
2358 | * None of them have a `name` property explicitly defined, but | |
2359 | * it is set by {@link CKEDITOR.editor#addCommand} and {@link CKEDITOR.ui#add}. | |
2360 | * | |
2361 | * During editor initialization all features that the editor should activate | |
2362 | * should be passed to {@link CKEDITOR.editor#addFeature} (shorthand for {@link CKEDITOR.filter#addFeature}). | |
2363 | * | |
2364 | * This method checks if a feature can be activated (see {@link #requiredContent}) and if yes, | |
2365 | * then it registers allowed content rules required by this feature (see {@link #allowedContent}) along | |
2366 | * with two kinds of transformations: {@link #contentForms} and {@link #contentTransformations}. | |
2367 | * | |
2368 | * By default all buttons that are included in [toolbar layout configuration](#!/guide/dev_toolbar) | |
2369 | * are checked and registered with {@link CKEDITOR.editor#addFeature}, all styles available in the | |
2370 | * 'Format' and 'Styles' drop-down lists are checked and registered too and so on. | |
2371 | * | |
2372 | * @since 4.1 | |
2373 | * @class CKEDITOR.feature | |
2374 | * @abstract | |
2375 | */ | |
2376 | ||
2377 | /** | |
2378 | * HTML code that can be generated by this feature. | |
2379 | * | |
2380 | * For example a basic image feature (image button displaying the image dialog window) | |
2381 | * may allow `'img[!src,alt,width,height]'`. | |
2382 | * | |
2383 | * During the feature activation this value is passed to {@link CKEDITOR.filter#allow}. | |
2384 | * | |
2385 | * @property {CKEDITOR.filter.allowedContentRules} [allowedContent=null] | |
2386 | */ | |
2387 | ||
2388 | /** | |
2389 | * Minimal HTML code that this feature must be allowed to | |
2390 | * generate in order to work. | |
2391 | * | |
2392 | * For example a basic image feature (image button displaying the image dialog window) | |
2393 | * needs `'img[src,alt]'` in order to be activated. | |
2394 | * | |
2395 | * During the feature validation this value is passed to {@link CKEDITOR.filter#check}. | |
2396 | * | |
2397 | * If this value is not provided, a feature will be always activated. | |
2398 | * | |
2399 | * @property {CKEDITOR.filter.contentRule} [requiredContent=null] | |
2400 | */ | |
2401 | ||
2402 | /** | |
2403 | * The name of the feature. | |
2404 | * | |
2405 | * It is used for example to identify which {@link CKEDITOR.filter#allowedContent} | |
2406 | * rule was added for which feature. | |
2407 | * | |
2408 | * @property {String} name | |
2409 | */ | |
2410 | ||
2411 | /** | |
2412 | * Feature content forms to be registered in the {@link CKEDITOR.editor#filter} | |
2413 | * during the feature activation. | |
2414 | * | |
2415 | * See {@link CKEDITOR.filter#addContentForms} for more details. | |
2416 | * | |
2417 | * @property [contentForms=null] | |
2418 | */ | |
2419 | ||
2420 | /** | |
2421 | * Transformations (usually for content generated by this feature, but not necessarily) | |
2422 | * that will be registered in the {@link CKEDITOR.editor#filter} during the feature activation. | |
2423 | * | |
2424 | * See {@link CKEDITOR.filter#addTransformations} for more details. | |
2425 | * | |
2426 | * @property [contentTransformations=null] | |
2427 | */ | |
2428 | ||
2429 | /** | |
2430 | * Returns a feature that this feature needs to register. | |
2431 | * | |
2432 | * In some cases, during activation, one feature may need to register | |
2433 | * another feature. For example a {@link CKEDITOR.ui.button} often registers | |
2434 | * a related command. See {@link CKEDITOR.ui.button#toFeature}. | |
2435 | * | |
2436 | * This method is executed when a feature is passed to the {@link CKEDITOR.editor#addFeature}. | |
2437 | * | |
2438 | * @method toFeature | |
2439 | * @returns {CKEDITOR.feature} | |
2440 | */ |