feeds_ex-8.x-1.0-alpha4/src/Feeds/Parser/ParserBase.php

src/Feeds/Parser/ParserBase.php
<?php

namespace Drupal\feeds_ex\Feeds\Parser;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\feeds\Exception\EmptyFeedException;
use Drupal\feeds\FeedInterface;
use Drupal\feeds\Feeds\Item\DynamicItem;
use Drupal\feeds\Feeds\Parser\ParserBase as FeedsParserBase;
use Drupal\feeds\Plugin\Type\MappingPluginFormInterface;
use Drupal\feeds\Plugin\Type\Parser\ParserInterface;
use Drupal\feeds\Result\FetcherResultInterface;
use Drupal\feeds\Result\ParserResult;
use Drupal\feeds\Result\ParserResultInterface;
use Drupal\feeds\StateInterface;
use Drupal\feeds_ex\Encoder\EncoderInterface;

/**
 * The Feeds extensible parser.
 */
abstract class ParserBase extends FeedsParserBase implements ParserInterface, PluginFormInterface, MappingPluginFormInterface {

  /**
   * The messenger, for compatibility with Drupal 8.5.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $feedsExMessenger;

  /**
   * The class used as the text encoder.
   *
   * @var string
   */
  protected $encoderClass = '\Drupal\feeds_ex\Encoder\TextEncoder';

  /**
   * The encoder used to convert encodings.
   *
   * @var \Drupal\feeds_ex\Encoder\EncoderInterface
   */
  protected $encoder;

  /**
   * The default list of HTML tags allowed by Xss::filter().
   *
   * In addition of \Drupal\Component\Utility\Xss::$htmlTags also the <pre>-tag
   * is added to the list of allowed tags. This is because for the JMESPath
   * parser an error can be generated that needs to be displayed preformatted.
   *
   * @var array
   *
   * @see \Drupal\Component\Utility\Xss::filter()
   */
  protected static $htmlTags = [
    'a',
    'em',
    'strong',
    'cite',
    'blockquote',
    'br',
    'pre',
    'code',
    'ul',
    'ol',
    'li',
    'dl',
    'dt',
    'dd',
  ];

  /**
   * A list of sources to parse.
   *
   * @var array
   */
  protected $sources;

  /**
   * Constructs a ParserBase object.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin id.
   * @param array $plugin_definition
   *   The plugin definition.
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
    if (!$this->hasConfigForm()) {
      unset($plugin_definition['form']['configuration']);
    }
    $this->sources = [];

    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * Returns rows to be parsed.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   Source information.
   * @param \Drupal\feeds\Result\FetcherResultInterface $fetcher_result
   *   The result returned by the fetcher.
   * @param \Drupal\feeds\StateInterface $state
   *   The state object.
   *
   * @return array|Traversable
   *   Some iterable that returns rows.
   */
  abstract protected function executeContext(FeedInterface $feed, FetcherResultInterface $fetcher_result, StateInterface $state);

  /**
   * Executes a single source expression.
   *
   * @param string $machine_name
   *   The source machine name being executed.
   * @param string $expression
   *   The expression to execute.
   * @param mixed $row
   *   The row to execute on.
   *
   * @return scalar|[]scalar
   *   Either a scalar, or a list of scalars. If null, the value will be
   *   ignored.
   */
  abstract protected function executeSourceExpression($machine_name, $expression, $row);

  /**
   * Validates an expression.
   *
   * @param string &$expression
   *   The expression to validate.
   *
   * @return string|null
   *   Return the error string, or null if validation was passed.
   */
  abstract protected function validateExpression(&$expression);

  /**
   * Returns the errors after parsing.
   *
   * @return array
   *   A structured array array with keys:
   *   - message: The error message.
   *   - variables: The variables for the message.
   *   - severity: The severity of the message.
   *
   * @see watchdog()
   */
  abstract protected function getErrors();

  /**
   * Allows subclasses to prepare for parsing.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   The feed we are parsing for.
   * @param \Drupal\feeds\Result\FetcherResultInterface $fetcher_result
   *   The result of the fetching stage.
   * @param \Drupal\feeds\StateInterface $state
   *   The state object.
   */
  protected function setUp(FeedInterface $feed, FetcherResultInterface $fetcher_result, StateInterface $state) {
  }

  /**
   * Allows subclasses to cleanup after parsing.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   The feed we are parsing for.
   * @param \Drupal\feeds\Result\ParserResultInterface $parser_result
   *   The result of parsing.
   * @param \Drupal\feeds\StateInterface $state
   *   The state object.
   */
  protected function cleanUp(FeedInterface $feed, ParserResultInterface $parser_result, StateInterface $state) {
  }

  /**
   * Starts internal error handling.
   *
   * Subclasses can override this to being error handling.
   */
  protected function startErrorHandling() {
  }

  /**
   * Stops internal error handling.
   *
   * Subclasses can override this to end error handling.
   */
  protected function stopErrorHandling() {
  }

  /**
   * Loads the necessary library.
   *
   * Subclasses can override this to load the necessary library. It will be
   * called automatically.
   *
   * @throws \RuntimeException
   *   Thrown if the library does not exist.
   */
  protected function loadLibrary() {
  }

  /**
   * Returns whether or not this parser uses a context query.
   *
   * Sub-classes can return false here if they don't require a user-configured
   * context query.
   *
   * @return bool
   *   True if the parser uses a context query and false if not.
   */
  protected function hasConfigurableContext() {
    return TRUE;
  }

  /**
   * Returns the label for single source.
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null
   *   A translated string if the source has a special name. Null otherwise.
   */
  protected function configSourceLabel() {
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function parse(FeedInterface $feed, FetcherResultInterface $fetcher_result, StateInterface $state) {
    $this->loadLibrary();
    $this->startErrorHandling();
    $result = new ParserResult();
    // @todo Find out what setting link in the D7 version of Feeds Extensible
    // Parsers meant and then determine whether or not this code is still needed
    // in some way.
    // @see Drupal\Tests\feeds_ex\Unit\Feeds\Parser\HtmlParserTest::testLinkIsSet()
    // @code
    // $fetcher_config = $feed->getConfigurationFor($feed->importer->fetcher);
    // phpcs:ignore Drupal.Files.LineLength.TooLong
    // $result->link = is_string($fetcher_config['source']) ? $fetcher_config['source'] : '';
    // @endcode
    try {
      $this->setUp($feed, $fetcher_result, $state);
      $this->parseItems($feed, $fetcher_result, $result, $state);
      $this->cleanUp($feed, $result, $state);
    }
    catch (EmptyFeedException $e) {
      // The feed is empty.
      $this->getMessenger()->addMessage($this->t('The feed is empty.'), 'warning', FALSE);
    }
    catch (\Exception $exception) {
      // Do nothing. Store for later.
    }

    // Display errors.
    $errors = $this->getErrors();
    $this->printErrors($errors, $this->configuration['display_errors'] ? RfcLogLevel::DEBUG : RfcLogLevel::ERROR);

    $this->stopErrorHandling();

    if (isset($exception)) {
      throw $exception;
    }

    return $result;
  }

  /**
   * Performs the actual parsing.
   *
   * @param \Drupal\feeds\FeedInterface $feed
   *   The feed source.
   * @param \Drupal\feeds\Result\FetcherResultInterface $fetcher_result
   *   The fetcher result.
   * @param \Drupal\feeds\Result\ParserResultInterface $result
   *   The parser result object to populate.
   * @param \Drupal\feeds\StateInterface $state
   *   The state object.
   */
  protected function parseItems(FeedInterface $feed, FetcherResultInterface $fetcher_result, ParserResultInterface $result, StateInterface $state) {
    $expressions = $this->prepareExpressions();
    $variable_map = $this->prepareVariables($expressions);

    foreach ($this->executeContext($feed, $fetcher_result, $state) as $row) {
      if ($item = $this->executeSources($row, $expressions, $variable_map)) {
        $result->addItem($item);
      }
    }
  }

  /**
   * Prepares the expressions for parsing.
   *
   * At this point we just remove empty expressions.
   *
   * @return array
   *   A map of machine name to expression.
   */
  protected function prepareExpressions() {
    $expressions = [];
    foreach ($this->sources as $machine_name => $source) {
      if (strlen($source['value'])) {
        $expressions[$machine_name] = $source['value'];
      }
    }

    return $expressions;
  }

  /**
   * Prepares the variable map used to substitution.
   *
   * @param array $expressions
   *   The expressions being parsed.
   *
   * @return array
   *   A map of machine name to variable name.
   */
  protected function prepareVariables(array $expressions) {
    $variable_map = [];
    foreach ($expressions as $machine_name => $expression) {
      $variable_map[$machine_name] = '$' . $machine_name;
    }
    return $variable_map;
  }

  /**
   * Executes the source expressions.
   *
   * @param mixed $row
   *   A single item returned from the context expression.
   * @param array $expressions
   *   A map of machine name to expression.
   * @param array $variable_map
   *   A map of machine name to variable name.
   *
   * @return array
   *   The fully-parsed item array.
   */
  protected function executeSources($row, array $expressions, array $variable_map) {
    $item = new DynamicItem();
    $variables = [];

    foreach ($expressions as $machine_name => $expression) {
      // Variable substitution.
      $expression = strtr($expression, $variables);

      $result = $this->executeSourceExpression($machine_name, $expression, $row);

      if ($result === NULL) {
        $variables[$variable_map[$machine_name]] = '';
        continue;
      }

      $item->set($machine_name, $result);
      $variables[$variable_map[$machine_name]] = is_array($result) ? reset($result) : $result;
    }

    return $item;
  }

  /**
   * Prints errors to the screen.
   *
   * @param array $errors
   *   A list of errors as returned by stopErrorHandling().
   * @param int $severity
   *   (optional) Limit to only errors of the specified severity. Defaults to
   *   RfcLogLevel::ERROR.
   *
   * @see watchdog()
   */
  protected function printErrors(array $errors, $severity = RfcLogLevel::ERROR) {
    foreach ($errors as $error) {
      if ($error['severity'] > $severity) {
        continue;
      }
      // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
      $this->getMessenger()->addMessage($this->t($error['message'], $error['variables']), $error['severity'] <= RfcLogLevel::ERROR ? 'error' : 'warning', FALSE);
    }
  }

  /**
   * Prepares the raw string for parsing.
   *
   * @param \Drupal\feeds\Result\FetcherResultInterface $fetcher_result
   *   The fetcher result.
   *
   * @return string
   *   The prepared raw string.
   */
  protected function prepareRaw(FetcherResultInterface $fetcher_result) {
    $raw = $fetcher_result->getRaw();

    // Check if the raw data is string. If not, abort.
    if (!is_string($raw)) {
      throw new EmptyFeedException();
    }

    $raw = $this->getEncoder()->convertEncoding($raw);

    // Strip null bytes.
    $raw = trim(str_replace("\0", '', $raw));

    // Check that the string has at least one character.
    if (!isset($raw[0])) {
      throw new EmptyFeedException();
    }

    return $raw;
  }

  /**
   * {@inheritdoc}
   */
  public function getMappingSources() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'context' => [
        'value' => '',
      ],
      'display_errors' => FALSE,
      'source_encoding' => ['auto'],
      'line_limit' => 100,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $encoding_form_state = $this->createSubFormState('encoding', $form_state);
    $form['encoding'] = $this->getEncoder()->buildConfigurationForm([], $encoding_form_state);
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    $encoding_form_state = $this->createSubFormState('encoding', $form_state);
    $this->getEncoder()->validateConfigurationForm($form['encoding'], $encoding_form_state);
    $form_state->setValue('encoding', $encoding_form_state->getValues());
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    // Preserve some configuration.
    $config = array_merge([
      'context' => $this->getConfiguration('context'),
    ], $form_state->getValues());

    $config += $config['encoding'];
    unset($config['encoding']);

    $this->setConfiguration($config);
  }

  /**
   * Creates a FormState object for subforms.
   *
   * @param string|array $key
   *   The form state key.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state to copy values from.
   *
   * @return \Drupal\Core\Form\FormStateInterface
   *   A new form state object.
   *
   * @see \Drupal\Core\Form\FormStateInterface::getValue()
   */
  protected function createSubFormState($key, FormStateInterface $form_state): FormStateInterface {
    // There might turn out to be other things that need to be copied and
    // passed. This works for now.
    return (new FormState())->setValues($form_state->getValue($key, []));
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormAlter(array &$form, FormStateInterface $form_state) {
    if ($this->hasConfigurableContext()) {
      $form['context'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Context'),
        '#default_value' => $this->configuration['context']['value'],
        '#description' => $this->t('The base query to run. See the <a href=":link" target="_new">Context query documentation</a> for more information.', [
          ':link' => 'https://www.drupal.org/node/3227985',
        ]),
        '#size' => 50,
        '#required' => TRUE,
        '#maxlength' => 1024,
        '#weight' => -50,
      ];
    }

    parent::mappingFormAlter($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormValidate(array &$form, FormStateInterface $form_state) {
    try {
      // Validate context.
      if ($this->hasConfigurableContext()) {
        if ($message = $this->validateExpression($form_state->getValue('context'))) {
          $message = new FormattableMarkup(Xss::filter($message, static::$htmlTags), []);
          $form_state->setErrorByName('context', $message);
        }
      }

      // Validate new sources.
      $mappings = $form_state->getValue('mappings');
      if (empty($mappings)) {
        return;
      }
      // Setup a list of select keys we are interested in.
      $select_keys = [];
      foreach ($this->getSupportedCustomSourcePlugins() as $custom_source_plugin_type) {
        $select_keys[] = 'custom__' . $custom_source_plugin_type;
      }
      foreach ($mappings as $i => $mapping) {
        foreach ($mapping['map'] as $subtarget => $map) {
          $select = $map['select'];
          // Check if a new custom source was added of a type we're interested
          // in.
          if (!in_array($select, $select_keys)) {
            // We're not interested in this selected source.
            continue;
          }
          // Check if a value was set for the custom source.
          if (!isset($map[$select]['value'])) {
            // No value was set for the custom source's value.
            continue;
          }
          if ($message = $this->validateExpression($map[$select]['value'])) {
            $message = new FormattableMarkup(Xss::filter($message, static::$htmlTags), []);
            $form_state->setErrorByName("mappings][$i][map][$subtarget][$select][value", $message);
          }
        }
      }
    }
    catch (\Exception $e) {
      // Exceptions due to missing libraries could occur, so catch these.
      $form_state->setError($form, $e->getMessage());
    }
  }

  /**
   * {@inheritdoc}
   */
  public function mappingFormSubmit(array &$form, FormStateInterface $form_state) {
    $config = [];

    // Set context.
    $config['context'] = [
      'value' => $form_state->getValue('context'),
    ];

    $this->setConfiguration($config);
  }

  /**
   * {@inheritdoc}
   */
  public function hasConfigForm() {
    return FALSE;
  }

  /**
   * Sets the encoder.
   *
   * @param \Drupal\feeds_ex\Encoder\EncoderInterface $encoder
   *   The encoder.
   *
   * @return $this
   *   The parser object.
   */
  public function setEncoder(EncoderInterface $encoder) {
    $this->encoder = $encoder;
    return $this;
  }

  /**
   * Returns the encoder.
   *
   * @return \Drupal\feeds_ex\Encoder\EncoderInterface
   *   The encoder object.
   */
  public function getEncoder() {
    if (!isset($this->encoder)) {
      $class = $this->encoderClass;
      $this->encoder = new $class($this->configuration['source_encoding']);
    }
    return $this->encoder;
  }

  /**
   * Sets the messenger.
   *
   * For compatibility with both Drupal 8.5 and Drupal 8.6.
   * Basically only useful for automated tests.
   *
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   */
  public function setFeedsExMessenger(MessengerInterface $messenger) {
    if (method_exists($this, 'setMessenger')) {
      $this->setMessenger($messenger);
    }
    else {
      $this->feedsExMessenger = $messenger;
    }
  }

  /**
   * Gets the messenger.
   *
   * For compatibility with both Drupal 8.5 and Drupal 8.6.
   *
   * @return \Drupal\Core\Messenger\MessengerInterface
   *   The messenger.
   */
  public function getMessenger() {
    if (method_exists($this, 'messenger')) {
      return $this->messenger();
    }
    if (isset($this->feedsExMessenger)) {
      return $this->feedsExMessenger;
    }
    return \Drupal::messenger();
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc