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