whitelabel-8.x-2.x-dev/src/Plugin/WhiteLabelNegotiation/WhiteLabelNegotiationUrl.php
src/Plugin/WhiteLabelNegotiation/WhiteLabelNegotiationUrl.php
<?php
namespace Drupal\whitelabel\Plugin\WhiteLabelNegotiation;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\whitelabel\WhiteLabelNegotiationMethodBase;
use Drupal\whitelabel\WhiteLabelNegotiationMethodInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class for identifying white labels via URL.
*
* @WhiteLabelNegotiation(
* id = \Drupal\whitelabel\Plugin\WhiteLabelNegotiation\WhiteLabelNegotiationUrl::METHOD_ID,
* label = @Translation("URL"),
* description = @Translation("White label from the URL."),
* weight = 0,
* )
*/
class WhiteLabelNegotiationUrl extends WhiteLabelNegotiationMethodBase implements WhiteLabelNegotiationMethodInterface, InboundPathProcessorInterface, OutboundPathProcessorInterface {
use StringTranslationTrait;
/**
* The white label negotiation method id.
*/
const METHOD_ID = 'url';
/**
* White label negotiation: use a query parameter as whitelabel indicator.
*/
const CONFIG_QUERY_PARAMETER = 'query_parameter';
/**
* White label negotiation: use the path prefix as whitelabel indicator.
*/
const CONFIG_PATH_PREFIX = 'path_prefix';
/**
* White label negotiation: use the domain as whitelabel indicator.
*/
const CONFIG_DOMAIN = 'domain';
/**
* Internal flag for knowing if the current request is already processed.
*
* @var bool
*/
protected $whiteLabelInboundProcessing;
/**
* Helper function for fetching all white label modes.
*
* @return string[]
* An array of descriptions, keyed by the mode system name.
*/
public static function getModes() {
return [
self::CONFIG_QUERY_PARAMETER => t('Query parameter (<em>example.com/somepage?token=<strong>whitelabel_token</strong></em>)'),
self::CONFIG_DOMAIN => t('Domain (<em><strong>whitelabel_token</strong>.example.com/somepage</em>)'),
// @todo This is very resource heavy and needs multiple lookups to
// determine if the site is in white label mode or not. Disabled for now
// as it did not have the desired effect.
// self::CONFIG_PATH_PREFIX => t('Path prefix (<em>example.com/<strong>whitelabel_token</strong>/somepage</em>)'),
];
}
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->whiteLabelInboundProcessing = FALSE;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'mode' => self::CONFIG_QUERY_PARAMETER,
'query_string_identifier' => 'store',
'domain' => '',
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function getWhiteLabel(Request $request = NULL) {
$whitelabel_mode = $this->configuration['mode'];
$token = NULL;
$whitelabel = NULL;
$invalid_whitelabel = FALSE;
switch ($whitelabel_mode) {
case self::CONFIG_QUERY_PARAMETER:
$query_string_identifier = $this->configuration['query_string_identifier'];
$token = $request->query->get($query_string_identifier) ?: NULL;
// Try to load the white label from the query parameter.
$whitelabel = $this->whiteLabelProvider->getWhiteLabelByToken($token);
break;
case self::CONFIG_PATH_PREFIX:
$request_path = urldecode(trim($request->getPathInfo(), '/'));
$path_args = explode('/', $request_path);
$token = array_shift($path_args);
// Rebuild a path with the remaining parts.
$temp_path = implode('/', $path_args);
$whitelabel = $this->whiteLabelProvider->getWhiteLabelByToken($token);
// This has toe be done like this to prevent circular references.
$path_validator = \Drupal::service('path.validator');
// White label found and valid remaining path.
if ($whitelabel && $path_validator->isValid($temp_path)) {
break;
}
// No white label and a valid initial path; treat this as a no
// white label request.
elseif (empty($whitelabel) && $path_validator->isValid($request_path)) {
return NULL;
}
// In all other cases there is something wrong.
else {
$invalid_whitelabel = TRUE;
}
break;
case self::CONFIG_DOMAIN:
// Get only the host, not the port.
$http_host = $request->getHost();
$host_parts = explode('.', $http_host);
// Make sure that the host was the token plus the base domain.
$normalized_base_url = str_replace(['https://', 'http://'], '', $this->configuration['domain']);
if ($http_host == $host_parts[0] . '.' . $normalized_base_url && $whitelabel = $this->whiteLabelProvider->getWhiteLabelByToken($host_parts[0])) {
$token = $host_parts[0];
}
// Do nothing if we are on the base domain.
elseif ($http_host == $normalized_base_url) {
break;
}
else {
$invalid_whitelabel = TRUE;
}
break;
}
// Return the white label if there is one.
if (!empty($whitelabel)) {
return $whitelabel;
}
// If the white label is invalid or permissions are wrong, show 404.
elseif ($invalid_whitelabel && ($whitelabel_mode == self::CONFIG_PATH_PREFIX || $whitelabel_mode == self::CONFIG_DOMAIN)) {
throw new NotFoundHttpException(sprintf("The mode '%s' and the discovered white label '%s' did not result in an existing page.", $whitelabel_mode, $token));
}
// In all other cases no white label was resolved.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
$whitelabel_mode = $this->configuration['mode'];
// This request is already being processed as part of the validation of the
// temporary path. Do nothing.
if ($this->whiteLabelInboundProcessing) {
return $path;
}
switch ($whitelabel_mode) {
case self::CONFIG_PATH_PREFIX:
// Strip the token from the path.
$parts = explode('/', trim($path, '/'));
$token = array_shift($parts);
// Rebuild a path with the remaining parts.
$temp_path = implode('/', $parts);
// Check if this is a valid path. The path validator will in turn call
// all path processors again. So add additional context to prevent
// infinite processing.
// If the page with a white label would result in an invalid path,
// continue with the current path instead and without white label. This
// prevents /node/add to be seen as white label 'node' and path '/add'.
$this->whiteLabelInboundProcessing = TRUE;
$whitelabel = $this->whiteLabelProvider->getWhiteLabelByToken($token);
// This has toe be done like this to prevent circular references.
$path_validator = \Drupal::service('path.validator');
// White label found and valid remaining path.
if ($whitelabel && $path_validator->isValid($temp_path)) {
$path = $temp_path;
}
// No white label and a valid initial path; treat this as a non
// white label request.
elseif (empty($whitelabel) && $path_validator->isValid($path)) {
return $path;
}
break;
}
return $path;
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
$url_scheme = 'http';
$port = 80;
if ($request) {
$url_scheme = $request->getScheme();
$port = $request->getPort();
}
// Get the white label from the options, or use the active one otherwise.
if (isset($options['whitelabel'])) {
$whitelabel = $options['whitelabel'];
}
else {
$whitelabel = $this->whiteLabelProvider->getWhiteLabel();
}
// No white label or no permission to serve it, so leave.
if (empty($whitelabel)) {
// Also add a cache context for no white label requests.
if ($bubbleable_metadata) {
$bubbleable_metadata->addCacheContexts(['whitelabel']);
}
// Revert the domain to the base domain to return from any possible white
// label.
if ($this->configuration['mode'] == self::CONFIG_DOMAIN) {
$options['base_url'] = $this->configuration['domain'];
// In case either the original base URL or the HTTP host contains a
// port, retain it.
if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) {
[, $port] = explode(':', $normalized_base_url);
$options['base_url'] .= ':' . $port;
}
elseif (($url_scheme == 'http' && $port != 80) || ($url_scheme == 'https' && $port != 443)) {
$options['base_url'] .= ':' . $port;
}
if (isset($options['https'])) {
if ($options['https'] === TRUE) {
$options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
}
elseif ($options['https'] === FALSE) {
$options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
}
}
}
return $path;
}
$whitelabel_token = $whitelabel->getToken();
// Apply white label in the right place.
$whitelabel_mode = $this->configuration['mode'];
switch ($whitelabel_mode) {
case self::CONFIG_QUERY_PARAMETER:
// Append the white label query parameter.
$query_string_identifier = $this->configuration['query_string_identifier'];
$options['query'][$query_string_identifier] = $whitelabel_token;
break;
case self::CONFIG_PATH_PREFIX:
// Append the white label token as a prefix, preserve existing prefixes.
// The weight of the inbound path processors defines the inbound order.
// (So make sure they match.)
$options['prefix'] = $whitelabel_token . '/' . $options['prefix'] . '/';
break;
case self::CONFIG_DOMAIN:
assert(!empty($this->configuration['domain']), 'Configuration value for the white label base domain value is missing.');
$options['base_url'] = $options['base_url'] ?? $this->configuration['domain'];
// Save the original base URL. If it contains a port, we need to
// retain it below.
if (!empty($options['base_url'])) {
// The colon in the URL scheme messes up the port checking below.
$normalized_base_url = str_replace(['https://', 'http://'], '', $options['base_url']);
}
// Ask for an absolute URL with our modified base URL.
$options['absolute'] = TRUE;
$options['base_url'] = $url_scheme . '://' . $whitelabel_token . '.' . $normalized_base_url;
// In case either the original base URL or the HTTP host contains a
// port, retain it.
if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) {
[, $port] = explode(':', $normalized_base_url);
$options['base_url'] .= ':' . $port;
}
elseif (($url_scheme == 'http' && $port != 80) || ($url_scheme == 'https' && $port != 443)) {
$options['base_url'] .= ':' . $port;
}
if (isset($options['https'])) {
if ($options['https'] === TRUE) {
$options['base_url'] = str_replace('http://', 'https://', $options['base_url']);
}
elseif ($options['https'] === FALSE) {
$options['base_url'] = str_replace('https://', 'http://', $options['base_url']);
}
}
// Add Drupal's sub-folder from the base_path if there is one.
$options['base_url'] .= rtrim(base_path(), '/');
break;
}
if ($bubbleable_metadata) {
$bubbleable_metadata->addCacheableDependency($whitelabel);
}
return $path;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$change_warning = $this->t('WARNING: CHANGING THIS DURING PRODUCTION WILL CAUSE EXISTING WHITE LABEL LINKS TO BREAK.');
$form['mode'] = [
'#type' => 'radios',
'#title' => $this->t('Detection mode'),
'#default_value' => $this->configuration['mode'],
'#description' => $this->t('The domain mode requires a white label DNS record and optionally a white label SSL certificate. <br>@warning', [
'@warning' => $change_warning,
]),
'#options' => self::getModes(),
];
$form['query_string_identifier'] = [
'#type' => 'machine_name',
'#title' => $this->t('Query string identifier'),
'#default_value' => $this->configuration['query_string_identifier'],
'#description' => $this->t("When using a query string, this defines the parameter to use. In the following example %example, the query string parameter would be 'token'. <br>@warning", [
'%example' => self::getModes()[self::CONFIG_QUERY_PARAMETER],
'@warning' => $change_warning,
]),
'#machine_name' => [
'exists' => [$this, 'validQueryString'],
'standalone' => TRUE,
],
'#states' => [
'visible' => [
':input[name="negotiator_settings[' . self::METHOD_ID . '][settings][mode]"]' => ['value' => self::CONFIG_QUERY_PARAMETER],
],
'required' => [
':input[name="negotiator_settings[' . self::METHOD_ID . '][settings][mode]"]' => ['value' => self::CONFIG_QUERY_PARAMETER],
],
],
];
$form['domain'] = [
'#type' => 'textfield',
'#title' => $this->t('Base domain'),
'#default_value' => $this->configuration['domain'],
'#description' => $this->t('When using subdomain-mode, this is used to determine the base domain.'),
'#states' => [
'visible' => [
':input[name="negotiator_settings[' . self::METHOD_ID . '][settings][mode]"]' => ['value' => self::CONFIG_DOMAIN],
],
'required' => [
':input[name="negotiator_settings[' . self::METHOD_ID . '][settings][mode]"]' => ['value' => self::CONFIG_DOMAIN],
],
],
'#attributes' => [
'placeholder' => 'https://example.com',
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Ensure that system reserved strings are not chosen.
if ($form_state->getValue('query_string_identifier') === 'q') {
$form_state->setErrorByName('query_string_identifier', $this->t('This is not a valid query string identifier. Please use something different than %query_string_identifier', ['@query_string_identifier' => $form_state->getValue('query_string_identifier')]));
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->setConfiguration($form_state->getValues());
}
/**
* Helper function for detecting if the provided query string is valid.
*
* @param string $id
* The ID to check.
*
* @return bool
* Boolean to indicate if the query string is already in use.
*/
public function validQueryString($id) {
// @todo ensure this actually checks something.
return FALSE;
}
}
