contacts_events-8.x-1.x-dev/modules/printing/src/Controller/TicketPrintingController.php
modules/printing/src/Controller/TicketPrintingController.php
<?php namespace Drupal\contacts_events_printing\Controller; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\Component\Utility\Crypt; use Drupal\contacts_events\Entity\EventClass; use Drupal\contacts_events\Entity\EventInterface; use Drupal\contacts_events\Entity\TicketInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\PrivateKey; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Site\Settings; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; /** * Booking checkout process controller. */ class TicketPrintingController extends ControllerBase { /** * The Event Class entity storage. * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $classStorage; /** * An array of class labels. * * @var string[] */ protected $classLabels; /** * Private Key service. * * @var \Drupal\Core\PrivateKey */ protected $privateKey; /** * Renderer service. * * @var \Drupal\Core\Render\RendererInterface */ protected $renderer; /** * Constructs a TicketPrintingController object. * * @param \Drupal\Core\Entity\EntityStorageInterface $class_storage * The Event Class entity storage class. * @param \Drupal\Core\PrivateKey $private_key * Private Key service. * @param \Drupal\Core\Render\RendererInterface $renderer * Renderer service. */ public function __construct(EntityStorageInterface $class_storage, PrivateKey $private_key, RendererInterface $renderer) { $this->classStorage = $class_storage; $this->privateKey = $private_key; $this->renderer = $renderer; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager')->getStorage('contacts_events_class'), $container->get('private_key'), $container->get('renderer') ); } /** * Generate a token for access checking to ticket render. * * This token needs to be reproducible across different session. */ public function generateRenderAccessToken($type, $id): string { // Build the value from our arguments. $value = 'cbtp:' . $type; if ($id) { $value .= ':' . $id; } return Crypt::hmacBase64($value, $this->privateKey->get() . Settings::getHashSalt()); } /** * A page for viewing the ticket and printing it if desired. */ public function viewTicket(TicketInterface $contacts_ticket): array { $content = []; $content['#attached']['library'][] = 'contacts_events_printing/ticket_viewing'; $content['content'] = $this->buildTicketOutput($contacts_ticket); return $content; } /** * A page for rendering the ticket contents for downloading as pdf. * * This page must be accessible by anonymous users with a valid token. */ public function renderTicket(TicketInterface $contacts_ticket): array { $content = []; $content['#attached']['library'][] = 'contacts_events_printing/ticket_printing'; $content['content'] = $this->buildTicketOutput($contacts_ticket); return $content; } /** * A page for rendering the ticket contents for downloading as pdf. * * This page must be accessible by anonymous users with a valid token. */ public function renderBooking(OrderInterface $commerce_order): array { $content = []; $content['#attached']['library'][] = 'contacts_events_printing/ticket_printing'; $content['content'] = []; // @todo Add a booking summary page. // Add each ticket to the booking. foreach ($commerce_order->get('order_items') as $order_item_field_item) { /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ $order_item = $order_item_field_item->entity; if ($order_item->bundle() !== 'contacts_ticket') { continue; } $allowed_states = ['paid_in_full']; if (!in_array($order_item->get('state')->first()->value, $allowed_states)) { continue; } /** @var \Drupal\contacts_events\Entity\TicketInterface $contacts_ticket */ $contacts_ticket = $order_item->getPurchasedEntity(); $content['content'][] = $this->buildTicketOutput($contacts_ticket); } return $content; } /** * Check event configuration for printing settings. * * @param \Drupal\contacts_events\Entity\EventInterface $event * The event to be checked. * * @return \Drupal\Core\Access\AccessResultInterface * Access result. */ public static function isEventPrintingEnabled(EventInterface $event): AccessResultInterface { if (!$event->getSetting('printing.enabled')) { return AccessResult::forbidden('Printing is not allowed for this event.') ->addCacheableDependency($event); } return AccessResult::neutral(); } /** * Access control handler for checking a valid token was supplied. */ public function renderTicketAccess(TicketInterface $contacts_ticket, string $token) { /** @var \Drupal\contacts_events\Entity\EventInterface $event */ $event = $contacts_ticket->getEvent(); $printing_enabled = static::isEventPrintingEnabled($event); if ($printing_enabled->isForbidden()) { return $printing_enabled; } $check = $this->generateRenderAccessToken('ticket', $contacts_ticket->id()); return AccessResult::allowedIf(hash_equals($check, $token)); } /** * Access control handler for checking a valid token was supplied. */ public function renderBookingAccess(OrderInterface $commerce_order, string $token) { /** @var \Drupal\contacts_events\Entity\EventInterface $event */ $event = $commerce_order->get('event')->entity; $printing_enabled = static::isEventPrintingEnabled($event); if ($printing_enabled->isForbidden()) { return $printing_enabled; } $check = $this->generateRenderAccessToken('booking', $commerce_order->id()); return AccessResult::allowedIf(hash_equals($check, $token)); } /** * Access control handler checking if user can download or print their ticket. */ public function ticketAccess(AccountInterface $account, TicketInterface $contacts_ticket) { /** @var \Drupal\contacts_events\Entity\EventInterface $event */ $event = $contacts_ticket->getEvent(); $printing_enabled = static::isEventPrintingEnabled($event); if ($printing_enabled->isForbidden()) { return $printing_enabled; } $allowed_states = ['paid_in_full']; $order_item = $contacts_ticket->getOrderItem(); if (!in_array($order_item->get('state')->first()->value, $allowed_states)) { return AccessResult::forbidden('This ticket is not in a state that can be printed.')->addCacheableDependency($order_item); } // Allow if we have the manage bookings permission. if ($account->hasPermission('can manage bookings for contacts_events')) { return AccessResult::allowed() ->addCacheContexts(['user.permissions']); } if ($account->isAuthenticated() && $account->id() == $contacts_ticket->getTicketHolderId()) { return AccessResult::allowed() ->addCacheContexts(['user']); } if ($account->isAuthenticated() && $account->id() == $contacts_ticket->getBooking()->getCustomerId()) { return AccessResult::allowed() ->addCacheContexts(['user']); } return AccessResult::neutral(); } /** * Access control handler checking if user can download/print a whole booking. */ public function bookingAccess(AccountInterface $account, OrderInterface $commerce_order) { if ($commerce_order->bundle() !== 'contacts_booking') { return AccessResult::forbidden('Printing is not allowed for non booking orders.')->addCacheableDependency($commerce_order); } /** @var \Drupal\contacts_events\Entity\EventInterface $event */ $event = $commerce_order->get('event')->entity; $printing_enabled = static::isEventPrintingEnabled($event); if ($printing_enabled->isForbidden()) { return $printing_enabled; } $disallowed_states = ['draft']; if (in_array($commerce_order->get('state')->first()->value, $disallowed_states)) { return AccessResult::forbidden('This ticket is not in a state that can be printed.')->addCacheableDependency($commerce_order); } // Allow if we have the manage bookings permission. if ($account->hasPermission('can manage bookings for contacts_events')) { return AccessResult::allowed() ->addCacheContexts(['user.permissions']); } if ($account->isAuthenticated() && $account->id() == $commerce_order->getCustomerId()) { return AccessResult::allowed() ->addCacheContexts(['user']); } // @todo Check group booking manager. return AccessResult::neutral(); } /** * Page callback for downloading a "printed" ticket in pdf form. */ public function printTicket(TicketInterface $contacts_ticket) { $ticket_id = $contacts_ticket->id(); $token = $this->generateRenderAccessToken('ticket', $ticket_id); $render_path = Url::fromUserInput('/booking/ticket/' . $ticket_id . '/' . $token . '/render', [ 'absolute' => TRUE, ])->toString(); $return = $this->generateDownload($render_path, DRUPAL_ROOT . '/../tickets/ticket-' . $ticket_id . '.pdf', DRUPAL_ROOT . '/../tickets/print_log'); if (!$return) { $this->messenger()->addError($this->t('There was a problem printing ticket %ticket: please contact an administrator.', [ '%ticket' => $ticket_id, ])); return []; } return $return; } /** * Page callback for downloading a "printed" ticket in pdf form. */ public function printBooking(OrderInterface $commerce_order) { $order_id = $commerce_order->id(); $token = $this->generateRenderAccessToken('booking', $order_id); $render_path = Url::fromUserInput('/booking/order/' . $order_id . '/' . $token . '/render', [ 'absolute' => TRUE, ])->toString(); $return = $this->generateDownload($render_path, DRUPAL_ROOT . '/../tickets/booking-' . $order_id . '.pdf', DRUPAL_ROOT . '/../tickets/print_log'); if (!$return) { $this->messenger()->addError($this->t('There was a problem printing booking %ticket: please contact an administrator.', [ '%ticket' => $order_id, ])); return []; } return $return; } /** * Builds the render array for the ticket to be shown. */ private function buildTicketOutput(TicketInterface $contacts_ticket): array { $booking = $contacts_ticket->getBooking(); $event = $contacts_ticket->getEvent(); $given_name = $contacts_ticket->get('name')->first()->given; // Check if ticket is full or segment. $text_field_name = 'ticket_text'; if ($contacts_ticket->hasField('segments') && !$contacts_ticket->get('segments')->isEmpty()) { $text_field_name = 'ticket_part_text'; } // Make sure the event has the correct text field. $letter_text = ''; if ($event->hasField($text_field_name)) { $letter_text = $event->get($text_field_name)->view([ 'label' => 'hidden', ]); $letter_text = $this->renderer->render($letter_text); $letter_text = str_replace('[name:given]', $given_name, $letter_text); } $address = ''; if (!$booking->get('shipping_profile')->isEmpty()) { $address = $booking->shipping_profile->entity->address->view([ 'label' => 'hidden', ]); } return [ '#theme' => 'ticket_printing_content', '#ticket' => $contacts_ticket, '#contact_id' => $contacts_ticket->get('contact')->target_id ?? '', '#order_id' => $booking->id(), '#ticket_id' => $contacts_ticket->id(), '#address' => $address, '#first_name' => $contacts_ticket->get('name')->first()->given, '#name' => $contacts_ticket->getName(), '#event_name' => $event->label(), '#event_date' => $event->get('date')->view([ 'label' => 'hidden', 'type' => 'daterange_custom', 'settings' => [ 'date_format' => 'l, F d, Y', 'separator' => 'to', ], ]), '#booking_reference' => $booking->getOrderNumber(), '#ticket_class' => $this->getClassLabel($contacts_ticket), '#letter_text' => ['#markup' => $letter_text], ]; } /** * Get the event class label for a ticket. * * @param \Drupal\contacts_events\Entity\TicketInterface $ticket * The ticket to retrieve for. * * @return string * The class label. */ protected function getClassLabel(TicketInterface $ticket) { if (!isset($this->classLabels)) { $this->classLabels = array_map(function (EventClass $class) { return $class->label(); }, $this->classStorage->loadMultiple()); } $class = $ticket->getMappedPrice()['class']; return $this->classLabels[$class] ?? '-'; } /** * Put the generated pdf file to the browser and exit. */ private function generateDownload($render_path, $file_path, $log_file) { // Make sure the directory exists. $directory = dirname($file_path); if (!is_dir($directory)) { mkdir($directory); } $options = [ // Quality adjustments. '--disable-smart-shrinking', '--print-media-type', // Disable borders. '-T 0', '-R 0', '-B 0', '-L 0', // Disable outlines. '--no-outline', '--outline-depth 0', ]; $options = implode(' ', $options); $script = "/usr/local/bin/wkhtmltopdf {$options} \"{$render_path}\" \"{$file_path}\" &> \"{$log_file}\""; $this->executePdfScript($script); if (!file_exists($file_path)) { $message = 'Failed to generate ticket PDF. Render path: %render_path. File path: %file_path. For more information, please see the log file %log_file'; $vars = [ '%render_path' => $render_path, '%file_path' => $file_path, '%log_file' => $log_file, ]; $this->getLogger('contacts_events_printing')->error($message, $vars); } else { return new BinaryFileResponse($file_path, 200, [ 'Content-Description' => 'File Transfer', 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename=' . basename($file_path), 'Content-Transfer-Encoding' => 'binary', 'Expires' => 0, 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 'Pragma' => 'public', 'Content-Length' => filesize($file_path), ]); } } /** * Executes the pdf generation command and returns the result. */ private function executePdfScript($command, &$output = [], &$retval = NULL) { // Store our path statically as it's not prone to changes. $path = drupal_static(__FUNCTION__); if (!isset($path)) { // Attempt a reliable method. if (function_exists('posix_getuid')) { $info = posix_getpwuid(posix_getuid()); $path = $info['dir']; } // Otherwise fall back on something not as reliable. else { $path = exec('echo $HOME'); } } // Correct our path if necessary. $orig = getenv('HOME'); if ($orig != $path) { putenv('HOME=' . $path); } // Pass on to exec(). $return = exec($command, $output, $retval); // Revert if overridden. if ($orig != $path) { putenv('HOME=' . $orig); } return $return; } }