views_streaming_data-8.x-1.x-dev/src/Plugin/views/display/StreamingDataExport.php

src/Plugin/views/display/StreamingDataExport.php
<?php

namespace Drupal\views_streaming_data\Plugin\views\display;

use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\State\StateInterface;
use Drupal\views\Plugin\views\display\PathPluginBase;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * The plugin that handles Data response callbacks for REST resources.
 *
 * @ingroup views_display_plugins
 *
 * @ViewsDisplay(
 *   id = "streaming_data_export",
 *   title = @Translation("Streaming data export"),
 *   help = @Translation("Create a streaming data export resource."),
 *   uses_route = TRUE,
 *   admin = @Translation("Streaming data export"),
 *   returns_response = TRUE
 * )
 */
class StreamingDataExport extends PathPluginBase implements StreamingDisplayInterface {

  /**
   * {@inheritdoc}
   */
  protected $usesAJAX = FALSE;

  /**
   * {@inheritdoc}
   */
  protected $usesPager = FALSE;

  /**
   * {@inheritdoc}
   */
  protected $usesMore = FALSE;

  /**
   * {@inheritdoc}
   */
  protected $usesAreas = FALSE;

  /**
   * {@inheritdoc}
   */
  protected $usesOptions = FALSE;

  /**
   * The mime type for the response.
   *
   * @var string
   */
  protected $mimeType = 'text/csv';

  /**
   * The file extension for the response.
   *
   * @var string
   */
  protected $fileExtension = 'txt';

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The collector of authentication providers.
   *
   * @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
   */
  protected $authenticationCollector;

  /**
   * The authentication providers, like 'cookie' and 'basic_auth'.
   *
   * @var string[]
   */
  protected $authenticationProviderIds;

  /**
   * The output stream.
   *
   * @var resource
   */
  protected $stream;

  /**
   * Constructs a StreamingDataExport object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
   *   The route provider.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state key value store.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param string[] $authentication_providers
   *   The authentication providers, keyed by ID.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);

    $this->renderer = $renderer;
    // $authentication_providers as defined in
    // \Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass
    // and as such it is an array, with authentication providers (cookie,
    // basic_auth) as keys and modules providing those as values (user,
    // basic_auth).
    $this->authenticationProviderIds = array_keys($authentication_providers);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('router.route_provider'),
      $container->get('state'),
      $container->get('renderer'),
      $container->getParameter('authentication_providers')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getPlugin($type) {
    if ($type !== 'query') {
      return parent::getPlugin($type);
    }
    // Look up the plugin name to use for this instance.
    $options = $this->getOption($type);

    // Return now if no options have been loaded.
    if (empty($options) || !isset($options['type'])) {
      return;
    }

    // Query plugins allow specifying a specific query class per base table.
    $views_data = Views::viewsData()->get($this->view->storage->get('base_table'));
    $name = $views_data['table']['base']['query_id'] ?? 'views_query';
    // Substitute the SQL plugin.
    if ($name === 'views_query') {
      $name = 'streaming_sql_query';
    }

    // Plugin instances are stored on the display for re-use.
    if (!isset($this->plugins[$type][$name])) {
      $plugin = Views::pluginManager($type)->createInstance($name);

      // Initialize the plugin.
      $plugin->init($this->view, $this, $options['options']);

      $this->plugins[$type][$name] = $plugin;
    }

    return $this->plugins[$type][$name];
  }

  /**
   * {@inheritdoc}
   */
  public function getType() {
    return 'streaming_data';
  }

  /**
   * {@inheritdoc}
   */
  public function usesExposed() {
    return TRUE;
  }

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

  /**
   * {@inheritdoc}
   */
  public function setMimeType($mime_type) {
    $this->mimeType = $mime_type;
  }

  /**
   * {@inheritdoc}
   */
  public function getMimeType() {
    return $this->mimeType;
  }

  /**
   * {@inheritdoc}
   */
  public function setFileExtension($extension) {
    $this->fileExtension = $extension;
  }

  /**
   * {@inheritdoc}
   */
  public function getFileExtension() {
    return $this->fileExtension;
  }

  /**
   * Gets the auth options available.
   *
   * @return string[]
   *   An array to use as value for "#options" in the form element.
   */
  public function getAuthOptions() {
    return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
  }

  /**
   * {@inheritdoc}
   */
  public function getExportFileName() {
    $path = (string) $this->getOption('path');
    $from_path = (bool) $this->getOption('export_file_from_path');
    $custom_name = $this->getOption('export_file_name');
    $extension = '.' . $this->getFileExtension();
    $name = NULL;
    if ($from_path) {
      $name = basename($path, $extension);
    }
    elseif ($custom_name) {
      $name = basename($custom_name, $extension);
    }
    if (!$name) {
      $name = $this->view->id() . '-' . $this->display['id'];
    }
    return $name . $extension;
  }

  /**
   * {@inheritdoc}
   */
  protected function defineOptions() {
    $options = parent::defineOptions();

    // Options for REST authentication.
    $options['auth'] = ['default' => []];
    $options['chunk_size'] = ['default' => self::DEFAULT_CHUNK_SIZE];
    $options['export_file_from_path'] = ['default' => FALSE];
    $options['export_file_name'] = ['default' => ''];

    // Set the default style plugin to 'csv_streaming_data'.
    $options['style']['contains']['type']['default'] = 'csv_streaming_data';
    $options['row']['contains']['type']['default'] = 'data_field';
    $options['defaults']['default']['style'] = FALSE;
    $options['defaults']['default']['row'] = FALSE;

    // Remove css and exposed form settings, as they are not used for the
    // data display.
    unset($options['exposed_form']);
    unset($options['exposed_block']);
    unset($options['css_class']);

    $options['pager']['type']['default'] = 'none';

    return $options;
  }

  /**
   * Set the preferred output stream.
   *
   * @param resource $stream
   *   A stream resource (file pointer) open for writing.
   */
  public function setOutputStream($stream) {
    $this->stream = $stream;
  }

  /**
   * Get the preferred output stream.
   *
   * @return resource
   *   A stream resource (file pointer) open for writing.
   */
  public function getOutputStream() {
    if (!isset($this->stream)) {
      $this->stream = fopen('php://output', 'wb');
    }
    return $this->stream;
  }

  /**
   * {@inheritdoc}
   */
  public function optionsSummary(&$categories, &$options) {
    parent::optionsSummary($categories, $options);

    // Authentication.
    $auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');

    unset($categories['page'], $categories['exposed']);
    // Hide some settings, as they aren't useful for pure data output.
    unset($options['show_admin_links'], $options['analyze-theme']);

    $categories['path'] = [
      'title' => $this->t('Path settings'),
      'column' => 'second',
      'build' => [
        '#weight' => -10,
      ],
    ];

    $options['path']['category'] = 'path';
    $options['path']['title'] = $this->t('Path');
    $options['auth'] = [
      'category' => 'path',
      'title' => $this->t('Authentication'),
      'value' => Unicode::truncate($auth, 24, FALSE, TRUE),
    ];

    $chunk_size = $this->getOption('chunk_size');
    $options['chunk_size'] = [
      'category' => 'format',
      'title' => $this->t('Chunk size'),
      'value' => $chunk_size,
    ];
    $options['export_file_name'] = [
      'category' => 'format',
      'title' => $this->t('File name'),
      'value' => $this->getExportFileName(),
    ];
    // Remove css/exposed form settings, as they are not used for the data
    // display. Remove pager settings.
    unset($options['exposed_form']);
    unset($options['exposed_block']);
    unset($options['css_class']);
    unset($options['pager']);
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);
    $section = $form_state->get('section');
    if ($section === 'auth') {
      $form['#title'] .= $this->t('The supported authentication methods for this view');
      $form['auth'] = [
        '#type' => 'checkboxes',
        '#title' => $this->t('Authentication methods'),
        '#description' => $this->t('These are the supported authentication providers for this view. When this view is requested, the client will be forced to authenticate with one of the selected providers. Make sure you set the appropriate requirements at the <em>Access</em> section since the Authentication System will fallback to the anonymous user if it fails to authenticate. For example: require Access: Role | Authenticated User.'),
        '#options' => $this->getAuthOptions(),
        '#default_value' => $this->getOption('auth'),
      ];
    }
    elseif ($section === 'chunk_size') {
      $form['chunk_size'] = [
        '#type' => 'select',
        '#title' => $this->t('Chunk size'),
        '#description' => $this->t('Chunk size to use when generating file.'),
        '#options' => [5 => 5, 10 => 10, 20 => 20, 50 => 50, 100 => 100, 200 => 200],
        '#default_value' => $this->options['chunk_size'],
      ];
    }
    elseif ($section === 'export_file_name') {
      $form['export_file_from_path'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('File name from path'),
        '#description' => $this->t('If this is not selected and no custom file name is supplied, a file name will be generated from the view machine name.'),
        '#default_value' => $this->options['export_file_from_path'],
      ];
      $form['export_file_name'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Custom file name'),
        '#default_value' => $this->options['export_file_name'],
        '#states' => [
          'visible' => [
            ':input[name="export_file_from_path"]' => ['checked' => FALSE],
          ],
        ],
      ];
      $form['info'] = [
        '#markup' => $this->t('File extension will be added based on serializer format.'),
      ];
    }
    elseif ($section === 'row') {
      // Limit row options to data_field in UI.
      $form['row']['type']['#options'] = array_intersect_key($form['row']['type']['#options'], ['data_field' => TRUE]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitOptionsForm(&$form, FormStateInterface $form_state) {
    parent::submitOptionsForm($form, $form_state);

    $section = $form_state->get('section');
    if ($section === 'auth') {
      $this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
    }
    elseif ($section === 'chunk_size') {
      $this->setOption('chunk_size', $form_state->getValue('chunk_size'));
    }
    elseif ($section === 'export_file_name') {
      $this->setOption('export_file_from_path', $form_state->getValue('export_file_from_path'));
      if ($form_state->getValue('export_file_from_path')) {
        // Force this to be empty.
        $this->setOption('export_file_name', '');
      }
      else {
        $this->setOption('export_file_name', $form_state->getValue('export_file_name'));
      }

    }
  }

  /**
   * {@inheritdoc}
   */
  public function collectRoutes(RouteCollection $collection) {
    parent::collectRoutes($collection);
    $view_id = $this->view->storage->id();
    $display_id = $this->display['id'];

    if ($route = $collection->get("view.$view_id.$display_id")) {
      // Data exports should only respond to GET methods.
      $route->setMethods(['GET']);

      $style_plugin = $this->getPlugin('style');
      // Format as a string using pipes as a delimiter.
      $route->setRequirement('_format', $style_plugin->getFormat());

      // Add authentication to the route if it was set. If no authentication was
      // set, the default authentication will be used, which is cookie based by
      // default.
      $auth = $this->getOption('auth');
      if (!empty($auth)) {
        $route->setOption('_auth', $auth);
      }
    }
  }

  /**
   * Determines whether the view overrides the given route.
   *
   * @param string $view_path
   *   The path of the view.
   * @param \Symfony\Component\Routing\Route $view_route
   *   The route of the view.
   * @param \Symfony\Component\Routing\Route $route
   *   The route itself.
   *
   * @return bool
   *   TRUE, when the view should override the given route.
   *
   * @see \Drupal\rest\Plugin\views\display\RestExport::overrideApplies()
   */
  protected function overrideApplies($view_path, Route $view_route, Route $route) {
    $route_has_format = $route->hasRequirement('_format');
    $route_formats = $route_has_format ? explode('|', $route->getRequirement('_format')) : [];
    $view_route_formats = $view_route->hasRequirement('_format') ? explode('|', $view_route->getRequirement('_format')) : [];
    return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
      && (!$route_has_format || array_intersect($route_formats, $view_route_formats) !== []);
  }

  /**
   * {@inheritdoc}
   */
  public static function buildResponse($view_id, $display_id, array $args = []) {
    /** @var \Drupal\views_streaming_data\StreamingViewExecutable $view */
    $view = \Drupal::service('views_streaming_data.views.executable')->get($view_id);
    $view->setDisplay($display_id);
    // Call this init here so that the style can set the mime type and extension
    // on the display.
    $view->initStyle();
    /** @var \Drupal\views_streaming_data\Plugin\views\display\StreamingDisplayInterface $display */
    $display = $view->getDisplay();

    $filename = $display->getExportFileName();
    set_time_limit(0);
    $response = new StreamedResponse(function () use ($view, $display_id, $args) {
      if ($view->access($display_id)) {
        $build = $view->executeDisplay($display_id, $args);
        /** @var \Drupal\views_streaming_data\Plugin\views\display\StreamingDisplayInterface $display */
        $display = $view->getDisplay();
        $stream = $display->getOutputStream();
        // This is likely not needed, but just in case there is markup.
        fwrite($stream, $build['#markup']);
        fclose($stream);
      }
    },
      StreamedResponse::HTTP_OK,
      [
        'Content-Type' => $display->getMimeType() . '; charset=utf-8',
        'Content-Disposition' => 'attachment; filename="' . $filename . '"',
        'Cache-Control' => 'must-revalidate, no-cache, private',
        'Max-Age' => '0',
      ]);
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function execute() {
    parent::execute();

    return $this->view->render();
  }

  /**
   * {@inheritdoc}
   */
  public function render() {
    $build = [];
    // This is expected to return an empty string since we are sending the
    // output to the browser in the context of the streaming response.
    $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
      return $this->view->style_plugin->render();
    });
    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function preview() {
    $build = ['#markup' => ''];
    if (!empty($this->view->live_preview)) {
      $build = [
        '#prefix' => '<strong>',
        '#plain_text' => $this->t('* No preview is available for a streaming export. *'),
        '#suffix' => '</strong>',
      ];
      // Make the SQL query visible for debugging.
      $this->view->build();
    }
    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    $dependencies = parent::calculateDependencies();

    $dependencies += ['module' => []];
    $dependencies['module'] = array_merge($dependencies['module'], array_filter(array_map(function ($provider) {
      // During the update path the provider options might be wrong. This can
      // happen when any update function, like block_update_8300() triggers a
      // view to be saved.
      return $this->authenticationProviderIds[$provider] ?? NULL;
    }, $this->getOption('auth'))));

    return $dependencies;
  }

}

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

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