|
@@ -0,0 +1,376 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Drupal\url_replace_filter\Plugin\Filter;
|
|
|
+
|
|
|
+use Drupal\Core\Form\FormStateInterface;
|
|
|
+use Drupal\Component\Utility\Html;
|
|
|
+use Drupal\Core\Messenger\MessengerInterface;
|
|
|
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
|
|
+use Drupal\Core\Routing\CurrentRouteMatch;
|
|
|
+use Drupal\Core\Url;
|
|
|
+use Drupal\filter\FilterProcessResult;
|
|
|
+use Drupal\filter\Plugin\FilterBase;
|
|
|
+use Symfony\Component\DependencyInjection\ContainerInterface;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Text filter for highlighting PHP source code.
|
|
|
+ *
|
|
|
+ * @Filter(
|
|
|
+ * id = "url_replace_filter",
|
|
|
+ * description = @Translation("Allows administrators to replace the base URL in <img> and <a> elements."),
|
|
|
+ * module = "url_replace_filter",
|
|
|
+ * title = @Translation("URL Replace filter"),
|
|
|
+ * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
|
|
|
+ * settings = {
|
|
|
+ * "replacements" = ''
|
|
|
+ * }
|
|
|
+ * )
|
|
|
+ */
|
|
|
+class UrlReplaceFilter extends FilterBase implements ContainerFactoryPluginInterface {
|
|
|
+
|
|
|
+ const SETTING_NAME = 'replacements';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var \Drupal\Core\Routing\CurrentRouteMatch
|
|
|
+ */
|
|
|
+ protected $currentRouteMatch;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var \Drupal\Core\Messenger\MessengerInterface
|
|
|
+ */
|
|
|
+ protected $messenger;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * UrlReplaceFilter constructor.
|
|
|
+ *
|
|
|
+ * @param array $configuration
|
|
|
+ * The plugin configuration.
|
|
|
+ * @param $plugin_id
|
|
|
+ * The plugin id.
|
|
|
+ * @param $plugin_definition
|
|
|
+ * The plugin definition.
|
|
|
+ * @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch
|
|
|
+ * The current_route_match service.
|
|
|
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
|
|
+ * The messenger service.
|
|
|
+ */
|
|
|
+ public function __construct(
|
|
|
+ array $configuration,
|
|
|
+ $plugin_id,
|
|
|
+ $plugin_definition,
|
|
|
+ CurrentRouteMatch $currentRouteMatch,
|
|
|
+ MessengerInterface $messenger
|
|
|
+ ) {
|
|
|
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
|
|
|
+ $this->currentRouteMatch = $currentRouteMatch;
|
|
|
+ $this->messenger = $messenger;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritdoc}
|
|
|
+ */
|
|
|
+ public static function create(
|
|
|
+ ContainerInterface $container,
|
|
|
+ array $configuration,
|
|
|
+ $plugin_id,
|
|
|
+ $plugin_definition
|
|
|
+ ) {
|
|
|
+ $messenger = $container->get('messenger');
|
|
|
+ $currentRouteMatch = $container->get('current_route_match');
|
|
|
+ return new static($configuration, $plugin_id, $plugin_definition, $currentRouteMatch, $messenger);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper to build rows in the table built by _url_replace_filter_settings().
|
|
|
+ *
|
|
|
+ * @param array $form
|
|
|
+ * The form array.
|
|
|
+ * @param int $index
|
|
|
+ * The format index.
|
|
|
+ * @param string $original
|
|
|
+ * The original value to be replaced.
|
|
|
+ * @param string $replacement
|
|
|
+ * The replacement for the original value.
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * A form array.
|
|
|
+ */
|
|
|
+ protected function buildRowForm(array $form, int $index, string $original, string $replacement) {
|
|
|
+ $form[self::SETTING_NAME]["replacement-{$index}"]['original'] = [
|
|
|
+ '#type' => 'textfield',
|
|
|
+ '#size' => 50,
|
|
|
+ '#default_value' => $original,
|
|
|
+ ];
|
|
|
+ $form[self::SETTING_NAME]["replacement-{$index}"]['replacement'] = [
|
|
|
+ '#type' => 'textfield',
|
|
|
+ '#size' => 50,
|
|
|
+ '#default_value' => $replacement,
|
|
|
+ ];
|
|
|
+
|
|
|
+ return $form;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritdoc}
|
|
|
+ */
|
|
|
+ public function settingsForm(array $form, FormStateInterface $form_state) {
|
|
|
+
|
|
|
+ $empty = 0;
|
|
|
+ $form[self::SETTING_NAME] = [
|
|
|
+ '#type' => 'details',
|
|
|
+ '#title' => $this->t('URL Replace Filter'),
|
|
|
+ '#open' => TRUE,
|
|
|
+ '#theme' => 'url_replace_filter_settings_form',
|
|
|
+ ];
|
|
|
+ $form['#element_validate'][] = [$this, 'settingsFormValidate'];
|
|
|
+
|
|
|
+ $stringSettings = $this->settings[self::SETTING_NAME];
|
|
|
+ $settings = $stringSettings ? array_values(unserialize($stringSettings)) : [];
|
|
|
+
|
|
|
+ foreach ((array) $settings as $index => $setting) {
|
|
|
+ $form = $this->buildRowForm($form, $index, $setting['original'], $setting['replacement']);
|
|
|
+ if (!$setting['original']) {
|
|
|
+ $empty++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Append up to 3 empty fields.
|
|
|
+ $index = count($settings);
|
|
|
+ while ($empty < 3) {
|
|
|
+ $form = $this->buildRowForm($form, $index, '', '');
|
|
|
+ $index++;
|
|
|
+ $empty++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $form;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Submit handler for _url_replace_filter_settings() form.
|
|
|
+ *
|
|
|
+ * Remove useless empty settings to keep variable as small as possible.
|
|
|
+ *
|
|
|
+ * Needs to be public to be usable as a #element_validate callback.
|
|
|
+ */
|
|
|
+ public function settingsFormValidate(array $form, FormStateInterface &$form_state) {
|
|
|
+ $settings = $form_state->getValue('filters')['url_replace_filter']['settings'][self::SETTING_NAME];
|
|
|
+
|
|
|
+ $validSettings = array_filter($settings, function (array $setting) {
|
|
|
+ return !(empty($setting['original']) && empty($setting['replacement']));
|
|
|
+ });
|
|
|
+
|
|
|
+ $result = serialize($validSettings);
|
|
|
+ $form_state->setValue(['filters', 'url_replace_filter', 'settings', self::SETTING_NAME], $result);
|
|
|
+
|
|
|
+ if (empty($validSettings)) {
|
|
|
+ $parameterName = 'filter_format';
|
|
|
+ /** @var \Drupal\filter\FilterFormatInterface $parameterValue */
|
|
|
+ $parameterValue = $this->currentRouteMatch->getParameter($parameterName);
|
|
|
+ $this->messenger->addMessage($this->t('URL Replace filter configuration is empty for @format: you could <a href=":edit">remove it</a> from this input format.', [
|
|
|
+ '@format' => $parameterValue->label(),
|
|
|
+ ':edit' => Url::fromRoute('entity.filter_format.edit_form', [
|
|
|
+ $parameterName => $parameterValue->id(),
|
|
|
+ ])->toString()]), MessengerInterface::TYPE_WARNING);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritdoc}
|
|
|
+ */
|
|
|
+ public function tips($long = FALSE) {
|
|
|
+ if ($long) {
|
|
|
+ return $this->t('To post pieces of code, surround them with <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.');
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return $this->t('You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * {@inheritdoc}
|
|
|
+ */
|
|
|
+ public function process($text, $langcode) {
|
|
|
+ // Escape code tags to prevent other filters from acting on them.
|
|
|
+ $text = preg_replace_callback('@<code>(.+?)</code>@s', [get_class($this), 'codeTagCallback'], $text);
|
|
|
+ $text = preg_replace_callback('@[\[<](\?php)(.+?)(\?)[\]>]@s', [get_class($this), 'phpTagCallback'], $text);
|
|
|
+
|
|
|
+ // Replace code.
|
|
|
+ $text = preg_replace_callback('@\[codefilter_code\](.+?)\[/codefilter_code\]@s', [get_class($this), 'processCodeCallback'], $text);
|
|
|
+ $text = preg_replace_callback('@\[codefilter_php\](.+?)\[/codefilter_php\]@s', [get_class($this), 'processPHPCallback'], $text);
|
|
|
+
|
|
|
+ // A hack, so we can conditionally nowrap based on filter settings.
|
|
|
+ // @todo Refactor how replacements are done so we can do this more cleanly.
|
|
|
+ if ($this->settings['nowrap_expand']) {
|
|
|
+ $text = str_replace('class="codeblock"', 'class="codeblock nowrap-expand"', $text);
|
|
|
+ }
|
|
|
+ return new FilterProcessResult($text);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Filter the given text.
|
|
|
+ */
|
|
|
+ public function _url_replace_filter_process($text, $format) {
|
|
|
+ $settings = _url_replace_filter_get_settings($format);
|
|
|
+ foreach ($settings as $setting) {
|
|
|
+ if ($setting['original']) {
|
|
|
+ $pattern = '!((<a\s[^>]*href)|(<img\s[^>]*src))\s*=\s*"' . preg_quote($setting['original']) . '!iU';
|
|
|
+ if (preg_match_all($pattern, $text, $matches)) {
|
|
|
+ $replacement = str_replace('%baseurl', rtrim(base_path(), '/'), $setting['replacement']);
|
|
|
+ foreach ($matches[0] as $key => $match) {
|
|
|
+ $text = str_replace($match, $matches[1][$key] . '="' . $replacement, $text);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Callback to replace content of the <code> elements.
|
|
|
+ *
|
|
|
+ * @param array $matches
|
|
|
+ * An array of matches passed by preg_replace_callback().
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * A formatted string.
|
|
|
+ */
|
|
|
+ public static function processCodeCallback(array $matches) {
|
|
|
+ return self::processCode($matches[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Callback to replace content of the <?php ?> elements.
|
|
|
+ *
|
|
|
+ * @param array $matches
|
|
|
+ * An array of matches passed by preg_replace_callback().
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * A formatted string.
|
|
|
+ */
|
|
|
+ public static function processPHPCallback(array $matches) {
|
|
|
+ return self::processPHP($matches[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Escape code blocks.
|
|
|
+ *
|
|
|
+ * @param string $text
|
|
|
+ * The string to escape.
|
|
|
+ * @param string $type
|
|
|
+ * The type of code block, either 'code' or 'php'.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * The escaped string.
|
|
|
+ */
|
|
|
+ public static function escape($text, $type = 'code') {
|
|
|
+ // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
|
|
|
+ $text = Html::escape(str_replace('\"', '"', $text));
|
|
|
+
|
|
|
+ // Protect newlines from line break converter.
|
|
|
+ $text = str_replace(["\r", "\n"], ['', ' '], $text);
|
|
|
+
|
|
|
+ // Add codefilter escape tags.
|
|
|
+ $text = "[codefilter_$type]{$text}[/codefilter_$type]";
|
|
|
+
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Processes chunks of escaped code into HTML.
|
|
|
+ */
|
|
|
+ public static function processCode($text) {
|
|
|
+ // Undo linebreak escaping.
|
|
|
+ $text = str_replace(' ', "\n", $text);
|
|
|
+ // Inline or block level piece?
|
|
|
+ $multiline = strpos($text, "\n") !== FALSE;
|
|
|
+ // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
|
|
|
+ $text = preg_replace("/^\n/", '', preg_replace('@</?(br|p)\s*/?>@', '', str_replace('\"', '"', $text)));
|
|
|
+ // Trim leading and trailing linebreaks.
|
|
|
+ $text = trim($text, "\n");
|
|
|
+ // Escape newlines.
|
|
|
+ $text = nl2br($text);
|
|
|
+
|
|
|
+ // PHP code in regular code.
|
|
|
+ $text = preg_replace_callback('/<\?php.+?\?>/s', [get_class(), 'processPHPInline'], $text);
|
|
|
+
|
|
|
+ $text = '<code>' . self::fixSpaces(str_replace(' ', ' ', $text)) . '</code>';
|
|
|
+ $text = $multiline ? '<div class="codeblock">' . $text . '</div>' : $text;
|
|
|
+ // Remove newlines to avoid clashing with the linebreak filter.
|
|
|
+ return str_replace("\n", '', $text);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper function for processCode().
|
|
|
+ */
|
|
|
+ public static function processPHPInline($matches) {
|
|
|
+ // Undo nl2br.
|
|
|
+ $text = str_replace('<br />', '', $matches[0]);
|
|
|
+ // Decode entities (the highlighter re-entifies) and highlight text.
|
|
|
+ $text = highlight_string(Html::decodeEntities($text), 1);
|
|
|
+ // Remove PHPs own added code tags.
|
|
|
+ $text = str_replace(['<code>', '</code>', "\n"], ['', '', ''], $text);
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Processes chunks of escaped PHP code into HTML.
|
|
|
+ */
|
|
|
+ public static function processPHP($text) {
|
|
|
+ // Note, pay attention to odd preg_replace-with-/e behaviour on slashes.
|
|
|
+ // Undo possible linebreak filter conversion.
|
|
|
+ $text = preg_replace('@</?(br|p)\s*/?>@', '', str_replace('\"', '"', $text));
|
|
|
+ // Undo the escaping in the prepare step.
|
|
|
+ $text = Html::decodeEntities($text);
|
|
|
+ // Trim leading and trailing linebreaks.
|
|
|
+ $text = trim($text, "\r\n");
|
|
|
+ // Highlight as PHP.
|
|
|
+ $text = '<div class="codeblock">' . highlight_string("<?php\n$text\n?>", 1) . '</div>';
|
|
|
+ // Remove newlines to avoid clashing with the linebreak filter.
|
|
|
+ $text = str_replace("\n", '', $text);
|
|
|
+ return self::fixSpaces($text);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Replace html space elements with literal space characters.
|
|
|
+ *
|
|
|
+ * @param string $text
|
|
|
+ * A string to fix spaces for.
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * A formatted string.
|
|
|
+ */
|
|
|
+ public static function fixSpaces($text) {
|
|
|
+ $text = preg_replace('@ (?! )@', ' ', $text);
|
|
|
+ // A single space before text is ignored by browsers. If a single space
|
|
|
+ // follows a break tag, replace it with a non-breaking space.
|
|
|
+ $text = preg_replace('@<br /> ([^ ])@', '<br /> $1', $text);
|
|
|
+ return $text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Callback to escape content of <code> tags.
|
|
|
+ *
|
|
|
+ * @param array $matches
|
|
|
+ * An array of matches passed by preg_replace_callback().
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * A formatted string.
|
|
|
+ */
|
|
|
+ public static function codeTagCallback(array $matches) {
|
|
|
+ return self::escape($matches[1], 'code');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Callback to escape content of <?php ?>, [?php ?], <% %>, and [% %] tags.
|
|
|
+ *
|
|
|
+ * @param array $matches
|
|
|
+ * An array of matches passed by preg_replace_callback().
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ * A formatted string.
|
|
|
+ */
|
|
|
+ public static function phpTagCallback(array $matches) {
|
|
|
+ return self::escape($matches[2], 'php');
|
|
|
+ }
|
|
|
+
|
|
|
+}
|