format; } /** * {@inheritdoc} */ public function filters($instance_id = NULL) { if (!isset($this->filterCollection)) { $this->filterCollection = new FilterPluginCollection(\Drupal::service('plugin.manager.filter'), $this->filters); $this->filterCollection->sort(); } if (isset($instance_id)) { return $this->filterCollection->get($instance_id); } return $this->filterCollection; } /** * {@inheritdoc} */ public function getPluginCollections() { return ['filters' => $this->filters()]; } /** * {@inheritdoc} */ public function setFilterConfig($instance_id, array $configuration) { $this->filters[$instance_id] = $configuration; if (isset($this->filterCollection)) { $this->filterCollection->setInstanceConfiguration($instance_id, $configuration); } return $this; } /** * {@inheritdoc} */ public function toArray() { $properties = parent::toArray(); // The 'roles' property is only used during install and should never // actually be saved. unset($properties['roles']); return $properties; } /** * {@inheritdoc} */ public function disable() { if ($this->isFallbackFormat()) { throw new \LogicException("The fallback text format '{$this->id()}' cannot be disabled."); } parent::disable(); // Allow modules to react on text format deletion. \Drupal::moduleHandler()->invokeAll('filter_format_disable', [$this]); // Clear the filter cache whenever a text format is disabled. filter_formats_reset(); return $this; } /** * {@inheritdoc} */ public function preSave(EntityStorageInterface $storage) { // Ensure the filters have been sorted before saving. $this->filters()->sort(); parent::preSave($storage); $this->name = trim($this->label()); } /** * {@inheritdoc} */ public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); // Clear the static caches of filter_formats() and others. filter_formats_reset(); if (!$update && !$this->isSyncing()) { // 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 // \Drupal\filter\FilterPermissions::permissions() and lastly // filter_formats(), so its cache must be reset upfront. if (($roles = $this->get('roles')) && $permission = $this->getPermissionName()) { foreach (user_roles() as $rid => $name) { $enabled = in_array($rid, $roles, TRUE); user_role_change_permissions($rid, [$permission => $enabled]); } } } } /** * 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; } /** * {@inheritdoc} */ public function getPermissionName() { return !$this->isFallbackFormat() ? 'use text format ' . $this->id() : FALSE; } /** * {@inheritdoc} */ public function getFilterTypes() { $filter_types = []; $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; } if ($filter->getType() === FilterInterface::TYPE_HTML_RESTRICTOR && $filter->getHTMLRestrictions() !== FALSE) { 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). elseif (!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). elseif (is_array($current_attributes) && $new_attributes == FALSE) { $intersection[$tag] = $new_attributes; } // The new filter allows more attributes; retain current. elseif (is_array($current_attributes) && $new_attributes == TRUE) { continue; } // The new filter allows the same attributes; retain current. elseif ($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'] = []; } } return $restrictions; } } /** * {@inheritdoc} */ public function removeFilter($instance_id) { unset($this->filters[$instance_id]); $this->filterCollection->removeInstanceId($instance_id); } /** * {@inheritdoc} */ public function onDependencyRemoval(array $dependencies) { $changed = parent::onDependencyRemoval($dependencies); $filters = $this->filters(); foreach ($filters as $filter) { // Remove disabled filters, so that this FilterFormat config entity can // continue to exist. if (!$filter->status && in_array($filter->provider, $dependencies['module'])) { $this->removeFilter($filter->getPluginId()); $changed = TRUE; } } return $changed; } /** * {@inheritdoc} */ protected function calculatePluginDependencies(PluginInspectionInterface $instance) { // Only add dependencies for plugins that are actually configured. This is // necessary because the filter plugin collection will return all available // filter plugins. // @see \Drupal\filter\FilterPluginCollection::getConfiguration() if (isset($this->filters[$instance->getPluginId()])) { parent::calculatePluginDependencies($instance); } } }