FilterFormat.php 12.8 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\filter\Entity\FilterFormat.
6
7
 */

8
namespace Drupal\filter\Entity;
9
10

use Drupal\Core\Config\Entity\ConfigEntityBase;
11
use Drupal\Core\Entity\EntityWithPluginBagsInterface;
12
use Drupal\Core\Entity\EntityStorageInterface;
13
use Drupal\filter\FilterFormatInterface;
14
use Drupal\filter\FilterBag;
15
use Drupal\filter\Plugin\FilterInterface;
16
17
18
19

/**
 * Represents a text format.
 *
20
 * @ConfigEntityType(
21
22
 *   id = "filter_format",
 *   label = @Translation("Text format"),
23
 *   controllers = {
24
 *     "form" = {
25
26
 *       "add" = "Drupal\filter\FilterFormatAddForm",
 *       "edit" = "Drupal\filter\FilterFormatEditForm",
27
28
 *       "disable" = "Drupal\filter\Form\FilterDisableForm"
 *     },
29
 *     "list_builder" = "Drupal\filter\FilterFormatListBuilder",
30
 *     "access" = "Drupal\filter\FilterFormatAccess",
31
 *   },
32
 *   config_prefix = "format",
33
 *   admin_permission = "administer filters",
34
35
36
 *   entity_keys = {
 *     "id" = "format",
 *     "label" = "name",
37
 *     "weight" = "weight",
38
 *     "status" = "status"
39
40
 *   },
 *   links = {
41
42
 *     "edit-form" = "entity.filter_format.edit_form",
 *     "disable" = "entity.filter_format.disable"
43
44
45
 *   }
 * )
 */
46
class FilterFormat extends ConfigEntityBase implements FilterFormatInterface, EntityWithPluginBagsInterface {
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

  /**
   * Unique machine name of the format.
   *
   * @todo Rename to $id.
   *
   * @var string
   */
  public $format;

  /**
   * Unique label of the text format.
   *
   * Since text formats impact a site's security, two formats with the same
   * label but different filter configuration would impose a security risk.
   * Therefore, each text format label must be unique.
   *
   * @todo Rename to $label.
   *
   * @var string
   */
  public $name;

  /**
   * Weight of this format in the text format selector.
   *
   * The first/lowest text format that is accessible for a user is used as
   * default format.
   *
   * @var int
   */
  public $weight = 0;

  /**
   * List of user role IDs to grant access to use this format on initial creation.
   *
   * This property is always empty and unused for existing text formats.
   *
   * Default configuration objects of modules and installation profiles are
   * allowed to specify a list of user role IDs to grant access to.
   *
   * This property only has an effect when a new text format is created and the
   * list is not empty. By default, no user role is allowed to use a new format.
   *
   * @var array
   */
  protected $roles;

  /**
   * Configured filters for this text format.
   *
   * An associative array of filters assigned to the text format, keyed by the
99
   * instance ID of each filter and using the properties:
100
   * - id: The plugin ID of the filter plugin instance.
101
102
   * - module: The name of the module providing the filter.
   * - status: (optional) A Boolean indicating whether the filter is
103
104
105
   *   enabled in the text format. Defaults to FALSE.
   * - weight: (optional) The weight of the filter in the text format. Defaults
   *   to 0.
106
   * - settings: (optional) An array of configured settings for the filter.
107
108
   *
   * Use FilterFormat::filters() to access the actual filters.
109
110
111
   *
   * @var array
   */
112
113
114
115
116
117
118
119
  protected $filters = array();

  /**
   * Holds the collection of filters that are attached to this format.
   *
   * @var \Drupal\filter\FilterBag
   */
  protected $filterBag;
120
121

  /**
122
   * {@inheritdoc}
123
124
125
126
127
128
   */
  public function id() {
    return $this->format;
  }

  /**
129
   * {@inheritdoc}
130
   */
131
  public function filters($instance_id = NULL) {
132
133
134
135
    if (!isset($this->filterBag)) {
      $this->filterBag = new FilterBag(\Drupal::service('plugin.manager.filter'), $this->filters);
      $this->filterBag->sort();
    }
136
    if (isset($instance_id)) {
137
      return $this->filterBag->get($instance_id);
138
    }
139
    return $this->filterBag;
140
141
142
143
144
  }

  /**
   * {@inheritdoc}
   */
145
146
  public function getPluginBags() {
    return array('filters' => $this->filters());
147
148
149
150
151
152
153
154
  }

  /**
   * {@inheritdoc}
   */
  public function setFilterConfig($instance_id, array $configuration) {
    $this->filters[$instance_id] = $configuration;
    if (isset($this->filterBag)) {
155
      $this->filterBag->setInstanceConfiguration($instance_id, $configuration);
156
    }
157
    return $this;
158
159
  }

160
  /**
161
162
   * {@inheritdoc}
   */
163
164
  public function toArray() {
    $properties = parent::toArray();
165
166
167
    // The 'roles' property is only used during install and should never
    // actually be saved.
    unset($properties['roles']);
168
169
170
171
172
    return $properties;
  }

  /**
   * {@inheritdoc}
173
174
175
176
177
   */
  public function disable() {
    parent::disable();

    // Allow modules to react on text format deletion.
178
    \Drupal::moduleHandler()->invokeAll('filter_format_disable', array($this));
179
180
181
182
183
184
185

    // Clear the filter cache whenever a text format is disabled.
    filter_formats_reset();

    return $this;
  }

186
187
188
  /**
   * {@inheritdoc}
   */
189
  public function preSave(EntityStorageInterface $storage) {
190
191
192
    // Ensure the filters have been sorted before saving.
    $this->filters()->sort();

193
    parent::preSave($storage);
194

195
196
197
198
199
200
    $this->name = trim($this->label());
  }

  /**
   * {@inheritdoc}
   */
201
202
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    parent::postSave($storage, $update);
203

204
205
206
    // Clear the static caches of filter_formats() and others.
    filter_formats_reset();

207
    if (!$update && !$this->isSyncing()) {
208
209
210
211
212
213
214
      // Default configuration of modules and installation profiles is allowed
      // to specify a list of user roles to grant access to for the new format;
      // apply the defined user role permissions when a new format is inserted
      // and has a non-empty $roles property.
      // Note: user_role_change_permissions() triggers a call chain back into
      // filter_permission() and lastly filter_formats(), so its cache must be
      // reset upfront.
215
      if (($roles = $this->get('roles')) && $permission = $this->getPermissionName()) {
216
217
218
219
220
221
222
        foreach (user_roles() as $rid => $name) {
          $enabled = in_array($rid, $roles, TRUE);
          user_role_change_permissions($rid, array($permission => $enabled));
        }
      }
    }
  }
223
224
225
226
227
228
229
230
231
232
233
234
235
236

  /**
   * Returns if this format is the fallback format.
   *
   * The fallback format can never be disabled. It must always be available.
   *
   * @return bool
   *   TRUE if this format is the fallback format, FALSE otherwise.
   */
  public function isFallbackFormat() {
    $fallback_format = \Drupal::config('filter.settings')->get('fallback_format');
    return $this->id() == $fallback_format;
  }

237
238
239
240
241
242
243
  /**
   * {@inheritdoc}
   */
  public function getPermissionName() {
    return !$this->isFallbackFormat() ? 'use text format ' . $this->id() : FALSE;
  }

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
  /**
   * {@inheritdoc}
   */
  public function getFilterTypes() {
    $filter_types = array();

    $filters = $this->filters();
    foreach ($filters as $filter) {
      if ($filter->status) {
        $filter_types[] = $filter->getType();
      }
    }

    return array_unique($filter_types);
  }

  /**
   * {@inheritdoc}
   */
  public function getHtmlRestrictions() {
    // Ignore filters that are disabled or don't have HTML restrictions.
    $filters = array_filter($this->filters()->getAll(), function($filter) {
      if (!$filter->status) {
        return FALSE;
      }
269
      if ($filter->getType() === FilterInterface::TYPE_HTML_RESTRICTOR && $filter->getHTMLRestrictions() !== FALSE) {
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
        return TRUE;
      }
      return FALSE;
    });

    if (empty($filters)) {
      return FALSE;
    }
    else {
      // From the set of remaining filters (they were filtered by array_filter()
      // above), collect the list of tags and attributes that are allowed by all
      // filters, i.e. the intersection of all allowed tags and attributes.
      $restrictions = array_reduce($filters, function($restrictions, $filter) {
        $new_restrictions = $filter->getHTMLRestrictions();

        // The first filter with HTML restrictions provides the initial set.
        if (!isset($restrictions)) {
          return $new_restrictions;
        }
        // Subsequent filters with an "allowed html" setting must be intersected
        // with the existing set, to ensure we only end up with the tags that are
        // allowed by *all* filters with an "allowed html" setting.
        else {
          // Track the union of forbidden (blacklisted) tags.
          if (isset($new_restrictions['forbidden_tags'])) {
            if (!isset($restrictions['forbidden_tags'])) {
              $restrictions['forbidden_tags'] = $new_restrictions['forbidden_tags'];
            }
            else {
              $restrictions['forbidden_tags'] = array_unique(array_merge($restrictions['forbidden_tags'], $new_restrictions['forbidden_tags']));
            }
          }

          // Track the intersection of allowed (whitelisted) tags.
          if (isset($restrictions['allowed'])) {
            $intersection = $restrictions['allowed'];
            foreach ($intersection as $tag => $attributes) {
              // If the current tag is not whitelisted by the new filter, then
              // it's outside of the intersection.
              if (!array_key_exists($tag, $new_restrictions['allowed'])) {
                // The exception is the asterisk (which applies to all tags): it
                // does not need to be whitelisted by every filter in order to be
                // used; not every filter needs attribute restrictions on all tags.
                if ($tag === '*') {
                  continue;
                }
                unset($intersection[$tag]);
              }
              // The tag is in the intersection, but now we must calculate the
              // intersection of the allowed attributes.
              else {
                $current_attributes = $intersection[$tag];
                $new_attributes = $new_restrictions['allowed'][$tag];
                // The current intersection does not allow any attributes, never
                // allow.
                if (!is_array($current_attributes) && $current_attributes == FALSE) {
                  continue;
                }
                // The new filter allows less attributes (all -> list or none).
                else if (!is_array($current_attributes) && $current_attributes == TRUE && ($new_attributes == FALSE || is_array($new_attributes))) {
                  $intersection[$tag] = $new_attributes;
                }
                // The new filter allows less attributes (list -> none).
                else if (is_array($current_attributes) && $new_attributes == FALSE) {
                  $intersection[$tag] = $new_attributes;
                }
                // The new filter allows more attributes; retain current.
                else if (is_array($current_attributes) && $new_attributes == TRUE) {
                  continue;
                }
                // The new filter allows the same attributes; retain current.
                else if ($current_attributes == $new_attributes) {
                  continue;
                }
                // Both list an array of attribute values; do an intersection,
                // where we take into account that a value of:
                //  - TRUE means the attribute value is allowed;
                //  - FALSE means the attribute value is forbidden;
                // hence we keep the ANDed result.
                else {
                  $intersection[$tag] = array_intersect_key($intersection[$tag], $new_attributes);
                  foreach (array_keys($intersection[$tag]) as $attribute_value) {
                    $intersection[$tag][$attribute_value] = $intersection[$tag][$attribute_value] && $new_attributes[$attribute_value];
                  }
                }
              }
            }
            $restrictions['allowed'] = $intersection;
          }

          return $restrictions;
        }
      }, NULL);

      // Simplification: if we have both a (intersected) whitelist and a (unioned)
      // blacklist, then remove any tags from the whitelist that also exist in the
      // blacklist. Now the whitelist alone expresses all tag-level restrictions,
      // and we can delete the blacklist.
      if (isset($restrictions['allowed']) && isset($restrictions['forbidden_tags'])) {
        foreach ($restrictions['forbidden_tags'] as $tag) {
          if (isset($restrictions['allowed'][$tag])) {
            unset($restrictions['allowed'][$tag]);
          }
        }
        unset($restrictions['forbidden_tags']);
      }

      // Simplification: if the only remaining allowed tag is the asterisk (which
      // contains attribute restrictions that apply to all tags), and only
      // whitelisting filters were used, then effectively nothing is allowed.
      if (isset($restrictions['allowed'])) {
        if (count($restrictions['allowed']) === 1 && array_key_exists('*', $restrictions['allowed']) && !isset($restrictions['forbidden_tags'])) {
          $restrictions['allowed'] = array();
        }
      }

      return $restrictions;
    }
  }

390
}