diff options
Diffstat (limited to 'sources/core/filter.js')
-rw-r--r-- | sources/core/filter.js | 2440 |
1 files changed, 2440 insertions, 0 deletions
diff --git a/sources/core/filter.js b/sources/core/filter.js new file mode 100644 index 00000000..c85b6beb --- /dev/null +++ b/sources/core/filter.js | |||
@@ -0,0 +1,2440 @@ | |||
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 | */ | ||