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;
}
}
