entrees block is displaying well and interact with map on mouse hover, map nodes are well drawn centered, map performance improvements, added domain contrib module and configured domain for mobile display

This commit is contained in:
Bachir Soussi Chiadmi
2018-01-04 11:55:52 +01:00
parent 7b9de31a42
commit 57a9bcd721
270 changed files with 24298 additions and 1127 deletions
@@ -0,0 +1,4 @@
allow_non_ascii: false
www_prefix: false
login_paths: '/user/login\r\n/user/password'
css_classes: ''
@@ -0,0 +1,24 @@
langcode: en
status: true
dependencies:
config:
- field.storage.user.field_domain_admin
module:
- user
id: user.user.field_domain_admin
field_name: field_domain_admin
entity_type: user
bundle: user
label: 'Domain administrator'
description: 'Select the domains this user may administer.'
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'domain:domain'
target_bundles: null
sort:
field: weight
direction: ASC
field_type: entity_reference
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- domain
- user
id: user.field_domain_admin
field_name: field_domain_admin
entity_type: user
type: entity_reference
settings:
target_type: domain
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- domain
id: domain_default_action
label: 'Set default domain record'
type: domain
plugin: domain_default_action
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- domain
id: domain_delete_action
label: 'Delete domain record'
type: domain
plugin: domain_delete_action
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- domain
id: domain_disable_action
label: 'Disable domain record'
type: domain
plugin: domain_disable_action
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- domain
id: domain_enable_action
label: 'Enable domain record'
type: domain
plugin: domain_enable_action
configuration: { }
@@ -0,0 +1,85 @@
# Schema for the configuration files of the Domain module.
domain.settings:
type: config_object
label: 'Domain settings'
mapping:
allow_non_ascii:
type: boolean
label: 'Allow non-ASCII characters'
www_prefix:
type: boolean
label: 'Ignore www prefix when negotiating domains'
login_paths:
type: text
label: 'Paths that should be accessible for inactive domains'
css_classes:
type: text
label: 'Custom css classes to apply to the body tag of each domain'
domain.record.*:
type: config_entity
label: 'Domain record'
mapping:
id:
type: string
label: 'ID'
uuid:
type: string
label: 'ID'
domain_id:
type: integer
label: 'Domain ID'
hostname:
type: string
label: 'Hostname'
name:
type: label
label: 'Name'
scheme:
type: string
label: 'Scheme'
status:
type: boolean
label: 'Status'
weight:
type: integer
label: 'Weight'
is_default:
type: boolean
label: 'Default domain'
action.configuration.domain_default_action:
type: action_configuration_default
label: 'Set default domain record'
action.configuration.domain_delete_action:
type: action_configuration_default
label: 'Delete domain record'
action.configuration.domain_disable_action:
type: action_configuration_default
label: 'Disable domain record'
action.configuration.domain_enable_action:
type: action_configuration_default
label: 'Enable domain record'
block.settings.domain_nav_block:
type: block_settings
label: 'Domain navigation block'
mapping:
link_options:
type: string
label: 'Link paths'
link_theme:
type: string
label: 'Link theme'
link_label:
type: string
label: 'Link text'
condition.plugin.domain:
type: condition.plugin
mapping:
domains:
type: sequence
sequence:
type: string
@@ -0,0 +1,138 @@
<?php
/**
* @file
* API documentation file for Domain module.
*/
/**
* Notifies other modules that we are loading a domain record from the database.
*
* When using this hook, you should invoke the namespace with:
*
* use Drupal\domain\DomainInterface;
*
* @param array $domains
* An array of $domain record objects.
*/
function hook_domain_load(array $domains) {
// Add a variable to the $domain.
foreach ($domains as $domain) {
$domain->addProperty('myvar', 'mydomainvar');
}
}
/**
* Allows modules to modify the inbound domain request.
*
* When using this hook, first check $domain->getMatchType(), which returns a
* numeric constant indicating the type of match derived by the caller or by
* earlier returns of this hook (such as domain_alias_request_alter()).
* Use this value to determine if the request needs to be overridden. Valid
* types are DomainNegotiator::DOMAIN_MATCH_NONE,
* DomainNegotiator::DOMAIN_MATCH_EXACT, DomainNegotiator::DOMAIN_MATCH_ALIAS.
*
* To issue a redirect, as in the case of Domain Alias, set a redirect
* property to a valid response code (301 or 302).
*
* @param \Drupal\domain\DomainInterface $domain
* A domain object defined by Drupal\domain\DomainInterface.
*/
function hook_domain_request_alter(\Drupal\domain\DomainInterface &$domain) {
// Add a special case to the example domain.
if ($domain->getMatchType() == \Drupal\domain\DomainNegotiator::DOMAIN_MATCH_EXACT && $domain->id() == 'example_com') {
// Do something here.
$domain->addProperty('foo', 'Bar');
}
}
/**
* Adds administrative operations for the domain overview form.
*
* These operations are only available to users who can administer the domain.
* That access check happens prior to this hook being called. If your use-case
* requires additional permission checking, you should provide it before
* returning any values.
*
* @param \Drupal\domain\DomainInterface $domain
* A domain record object.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account performing the operation.
*
* @return array
* An array of operations which uses a unique string key and requires the
* elements 'title' and 'url'; the 'query' value is optional, and used
* for link-actions with tokens
*/
function hook_domain_operations(\Drupal\domain\DomainInterface $domain, \Drupal\Core\Session\AccountInterface $account) {
$operations = [];
// Check permissions.
if ($account->hasPermission('view domain aliases') || $account->hasPermission('administer domain aliases')) {
// Add aliases to the list.
$id = $domain->id();
$operations['domain_alias'] = array(
'title' => t('Aliases'),
'url' => Url::fromRoute('domain_alias.admin', array('domain' => $id)),
'weight' => 60,
);
}
return $operations;
}
/**
* Alter the validation step of a domain record.
*
* This hook allows modules to change or extend how domain validation
* happens. Most useful for international domains or other special cases
* where a site wants to restrict domain creation is some manner.
*
* NOTE: This does not apply to Domain Alias records.
*
* @param array &$error_list
* The list of current validation errors. Modify this value by reference.
* If you return an empty array or NULL, the domain is considered valid.
* @param string $hostname
* The HTTP_HOST string value being validated, such as one.example.com.
* Note that this is checked for uniqueness separately. This value is not
* modifiable.
*
* No return value. Modify $error_list by reference. Return an empty array
* or NULL to validate this domain.
*
* @see domain_valid_domain()
*/
function hook_domain_validate_alter(&$error_list, $hostname) {
// Only allow TLDs to be .org for our site.
if (substr($hostname, -4) != '.org') {
$error_list[] = t('Only .org domains may be registered.');
}
}
/**
* Alter the list of domains that may be referenced.
*
* Note that this hook does not fire for users with the 'administer domains'
* permission.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $query
* An entity query prepared by DomainSelection::buildEntityQuery().
* @param \Drupal\Core\Session\AccountInterface $account
* The account of the user viewing the reference list.
* @param array $context
* A keyed array passing two items:
* - entity_type The type of entity (e.g. node, user) that requested the list.
* - bundle The entity subtype (e.g. 'article' or 'page').
* - field_type The access field group used for this selection. Groups are
* 'editor' for assigning editorial permissions (as in Domain Access)
* 'admin' for assigning administrative permissions for a specific domain.
* Most contributed modules will use 'editor'.
*
* No return value. Modify the $query object via methods.
*/
function hook_domain_references_alter($query, $account, $context) {
// Remove the default domain from non-admins when editing nodes.
if ($context['entity_type'] == 'node' && $context['field_type'] == 'editor' && !$account->hasPermission('edit assigned domains')) {
$default = \Drupal::service('entity_type.manager')->getStorage('domain')->loadDefaultId();
$query->condition('id', $default, '<>');
}
}
@@ -0,0 +1,641 @@
<?php
/**
* @file
* Drush commands for Domain Access.
*/
use Drupal\Component\Utility\Html;
use Drupal\domain\DomainInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Implements hook_drush_command().
*/
function domain_drush_command() {
$items = array();
$items['domain-list'] = array(
'description' => 'List active domains for the site.',
'examples' => array(
'drush domain-list',
'drush domains',
),
'aliases' => array('domains'),
);
$items['domain-add'] = array(
'description' => 'Add a new domain to the site.',
'examples' => array(
'drush domain-add example.com \'My Test Site\'',
'drush domain-add example.com \'My Test Site\' --inactive=1 --https==1',
'drush domain-add example.com \'My Test Site\' --weight=10',
'drush domain-add example.com \'My Test Site\' --validate=1',
),
'arguments' => array(
'hostname' => 'The domain hostname to register (e.g. example.com).',
'name' => 'The name of the site (e.g. Domain Two).',
),
'options' => array(
'inactive' => 'Set the domain to inactive status if set.',
'https' => 'Use https protocol for this domain if set.',
'weight' => 'Set the order (weight) of the domain.',
'is_default' => 'Set this domain as the default domain.',
'validate' => 'Force a check of the URL response before allowing registration.',
),
);
$items['domain-delete'] = array(
'description' => 'Delete a domain from the site.',
'examples' => array(
'drush domain-delete example.com',
'drush domain-delete 1',
),
'arguments' => array(
'domain' => 'The numeric id or hostname of the domain to delete.',
),
);
$items['domain-test'] = array(
'description' => 'Tests domains for proper response. If run from a subfolder, you must specify the --uri.',
'examples' => array(
'drush domain-test',
'drush domain-test example.com',
'drush domain-test 1',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to test. If no value is passed, all domains are tested.',
),
'options' => array(
'base_path' => 'The subdirectory name if Drupal is installed in a folder other than server root.',
),
);
$items['domain-default'] = array(
'description' => 'Sets the default domain. If run from a subfolder, you must specify the --uri.',
'examples' => array(
'drush domain-default example.com',
'drush domain-default 1',
'drush domain-default 1 --validate=1',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to make default.',
),
'options' => array(
'validate' => 'Force a check of the URL response before allowing registration.',
),
);
$items['domain-disable'] = array(
'description' => 'Sets a domain status to off.',
'examples' => array(
'drush domain-disable example.com',
'drush domain-disable 1',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to disable.',
),
);
$items['domain-enable'] = array(
'description' => 'Sets a domain status to on.',
'examples' => array(
'drush domain-disable example.com',
'drush domain-disable 1',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to enable.',
),
);
$items['domain-name'] = array(
'description' => 'Changes a domain label.',
'examples' => array(
'drush domain-name example.com Foo',
'drush domain-name 1 Foo',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to relabel.',
'name' => 'The name to use for the domain.',
),
);
$items['domain-machine-name'] = array(
'description' => 'Changes a domain name.',
'examples' => array(
'drush domain-machine-name example.com foo',
'drush domain-machine-name 1 foo',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to rename.',
'name' => 'The machine-readable name to use for the domain.',
),
);
$items['domain-scheme'] = array(
'description' => 'Changes a domain scheme.',
'examples' => array(
'drush domain-scheme example.com https',
'drush domain-scheme 1 https',
),
'arguments' => array(
'domain_id' => 'The numeric id or hostname of the domain to change.',
'scheme' => 'The URL schema (http or https) to use for the domain.',
),
);
$items['generate-domains'] = array(
'description' => 'Generate domains for testing.',
'arguments' => array(
'primary' => 'The primary domain to use. This will be created and used for *.example.com hostnames.',
),
'options' => array(
'count' => 'The count of extra domains to generate. Default is 15.',
'empty' => 'Pass empty=1 to truncate the {domain} table before creating records.'
),
'examples' => array(
'drush domain-generate example.com',
'drush domain-generate example.com --count=25',
'drush domain-generate example.com --count=25 --empty=1',
'drush gend',
'drush gend --count=25',
'drush gend --count=25 --empty=1',
),
'aliases' => array('gend'),
);
return $items;
}
/**
* Implements hook_drush_help().
*/
function domain_drush_help($section) {
$items = domain_drush_command();
$name = str_replace('domain:', '', $section);
if (isset($items[$name])) {
return dt($items[$name]['description']);
}
}
/**
* Shows the domain list.
*/
function drush_domain_list() {
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultipleSorted(NULL, TRUE);
if (empty($domains)) {
drush_print(dt('No domains have been created. Use drush domain-add to create one.'));
return;
}
$header = array(
'weight' => dt('Weight'),
'name' => dt('Name'),
'hostname' => dt('Hostname'),
'scheme' => dt('Scheme'),
'status' => dt('Status'),
'is_default' => dt('Default'),
'domain_id' => dt('Domain Id'),
'id' => dt('Machine name'),
);
$rows = array(array_values($header));
foreach ($domains as $domain) {
$row = array();
foreach ($header as $key => $name) {
$row[] = Html::escape($domain->get($key));
}
$rows[] = $row;
}
drush_print_table($rows, TRUE);
}
/**
* Generates a list of domains for testing.
*
* In my environment, I name hostnames one.* two.* up to ten. I also use
* foo.* bar.* and baz.*. We also want a non-hostname here and use
* myexample.com.
*
* The script may also add test1, test2, test3 up to any number to test a
* large number of domains. This test is mostly for UI testing.
*
* @param $primary
* The root domain to use for domain creation.
*
* @return
* A list of the domains created.
*/
function drush_domain_generate_domains($primary = 'example.com') {
// Check the number of domains to create.
$count = drush_get_option('count');
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
if (empty($count)) {
$count = 15;
}
// Ensure we don't duplicate any domains.
$existing = array();
if (!empty($domains)) {
foreach ($domains as $domain) {
$existing[] = $domain->getHostname();
}
}
// Set up one.* and so on.
$names = array(
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'foo',
'bar',
'baz',
);
// Set the creation array.
$new = array($primary);
foreach ($names as $name) {
$new[] = $name . '.' . $primary;
}
// Include a non hostname.
$new[] = 'my' . $primary;
// Filter against existing so we can count correctly.
$prepared = array();
foreach ($new as $key => $value) {
if (!in_array($value, $existing)) {
$prepared[] = $value;
}
}
// Add any test domains that have numeric prefixes. We don't expect these URLs to work,
// and mainly use these for testing the user interface.
$needed = $count - count($prepared);
for ($i = 1; $i <= $needed; $i++) {
$prepared[] = 'test' . $i . '.' . $primary;
}
// Get the initial item weight for sorting.
$start_weight = count($domains);
$prepared = array_slice($prepared, 0, $count);
// Create the domains.
foreach ($prepared as $key => $item) {
$hostname = strtolower($item);
$values = array(
'name' => ($item != $primary) ? ucwords(str_replace(".$primary", '', $item)) : \Drupal::config('system.site')->get('name'),
'hostname' => $hostname,
'scheme' => 'http',
'status' => 1,
'weight' => ($item != $primary) ? $key + $start_weight + 1 : -1,
'is_default' => 0,
'id' => \Drupal::service('entity_type.manager')->getStorage('domain')->createMachineName($hostname),
);
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->create($values);
domain_drush_create($domain);
}
// If nothing created, say so.
if (empty($new)) {
drush_print(dt('No new domains were created.'));
}
}
/**
* Validates the domain generation script.
*
* @param $primary
* The root domain to use for domain creation.
*/
function drush_domain_generate_domains_validate($primary = 'example.com') {
if ($empty = drush_get_option('empty')) {
db_query("TRUNCATE TABLE {domain}");
}
return;
// TODO: Update this validation.
$error = domain_valid_domain($primary);
if (!empty($error)) {
return drush_set_error('domain', $error);
}
}
/**
* Adds a new domain.
*
* @param string $hostname
* The domain name to register.
* @param string $name
* The name to use for this domain.
*/
function drush_domain_add($hostname, $name) {
$records_count = \Drupal::entityTypeManager()->getStorage('domain')->getQuery()->count()->execute();
$start_weight = $records_count + 1;
$hostname = strtolower($hostname);
/** @var \Drupal\domain\DomainStorageInterface $domain_storage */
$domain_storage = \Drupal::service('entity_type.manager')->getStorage('domain');
$values = array(
'hostname' => $hostname,
'name' => $name,
'status' => (!drush_get_option('invalid')) ? 1 : 0,
'scheme' => (!drush_get_option('https')) ? 'http' : 'https',
'weight' => ($weight = drush_get_option('weight')) ? $weight : $start_weight + 1,
'is_default' => ($is_default = drush_get_option('is_default')) ? $is_default : 0,
'id' => $domain_storage->createMachineName($hostname),
'validate_url' => (drush_get_option('validate')) ? 1 : 0,
);
$domain = $domain_storage->create($values);
domain_drush_create($domain);
}
/**
* Validates the domain add command arguments.
*
* @param string $hostname
* The domain name to register.
* @param string $name
* The name to use for this domain.
*
* @return bool
* TRUE when validation passed, FALSE otherwise.
*/
function drush_domain_add_validate($hostname, $name) {
$errors = domain_drush_validate_domain($hostname);
if (!empty($errors)) {
return drush_set_error('domain', $errors);
}
elseif (\Drupal::service('entity_type.manager')->getStorage('domain')->loadByHostname($hostname)) {
return drush_set_error('domain', dt('The hostname is already registered.'));
}
return TRUE;
}
/**
* Creates a domain record.
*
* @param Drupal\domain\DomainInterface $domain
* A domain entity.
*/
function domain_drush_create(DomainInterface $domain) {
if ($error = domain_drush_check_response($domain)) {
drush_set_error('hostname', $error);
}
else {
$domain->save();
if ($domain->getDomainId()) {
drush_print(dt('Created @name at @domain.', array('@name' => $domain->label(), '@domain' => $domain->getHostname())));
}
else {
drush_print(dt('The request could not be completed.'));
}
}
}
/**
* Runs a check to ensure that the domain is responsive.
*
* @param Drupal\domain\DomainInterface $domain
* A domain entity.
*
* @return string
* An error message if the domain url does not validate. Else empty.
*/
function domain_drush_check_response(DomainInterface $domain) {
// Check the domain response. First, clear the path value.
if ($domain->validate_url) {
$domain->setPath();
try {
$response = $domain->getResponse();
}
// We cannot know which Guzzle Exception class will be returned; be generic.
catch (RequestException $e) {
watchdog_exception('domain', $e);
// File a general server failure.
$domain->setResponse(500);
}
// If validate_url is set, then we must receive a 200 response.
if ($domain->getResponse() != 200) {
if (empty($response)) {
$response = 500;
}
return dt('The server request to @url returned a @response response. To proceed, disable the test of the server response by leaving off the --validate flag.', ['@url' => $domain->getPath(), '@response' => $response]);
}
}
}
/**
* Validates a domain.
*
* @param $hostname
* The domain name to validate for syntax and uniqueness.
*
* @return array
* An array of errors encountered.
*
* @see domain_validate()
*/
function domain_drush_validate_domain($hostname) {
/** @var \Drupal\domain\DomainValidatorInterface $validator */
$validator = \Drupal::service('domain.validator');
return $validator->validate($hostname);
}
/**
* Deletes a domain record.
*
* @param $argument
* The domain_id to delete. Pass 'all' to delete all records.
*/
function drush_domain_delete($argument = NULL) {
if (is_null($argument)) {
drush_set_error('domain', dt('You must specify a domain to delete.'));
}
if ($argument == 'all') {
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
if (empty($domains)) {
drush_print(dt('There are no domains to delete.'));
return;
}
$content = drush_choice(array(1 => dt('Delete all domains')), dt('This action may not be undone. Continue?'), '!value');
if (empty($content)) {
return;
}
}
// Resolve the domain.
elseif ($domain = drush_domain_get_from_argument($argument)) {
if ($domain->isDefault()) {
return drush_set_error('domain', dt('The primary domain may not be deleted. Use drush domain-default to set a new default domain.'));
}
$domains = [$domain];
}
else {
return;
}
foreach ($domains as $domain) {
$domain->delete();
drush_print(dt('Domain record @domain deleted.', array('@domain' => $domain->getHostname())));
}
return;
// TODO: Set options for re-assigning content.
$list = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
$options = array('0' => dt('Do not reassign'));
foreach ($list as $data) {
if ($data->id() != $domain->id()) {
$options[$data->getDomainId()] = $data->getHostname();
}
}
$content = drush_choice($options, dt('Reassign content to:'), '!value');
if (empty($content)) {
return;
}
$users = drush_choice($options, dt('Reassign users to:'), '!value');
if (empty($users)) {
return;
}
$values['domain_access'] = (!empty($content)) ? $content : 'none';
$values['domain_editor'] = (!empty($content)) ? $users : 'none';
domain_delete($domain, $values);
drush_print(dt('Domain record deleted.'));
}
/**
* Tests a domain record for the proper HTTP response.
*
* @param $argument
* The domain_id to test. Passing no value tests all records.
*/
function drush_domain_test($argument = NULL) {
// TODO: This won't work in a subdirectory without a parameter.
if ($base_path = drush_get_option('base_path')) {
$GLOBALS['base_path'] = '/' . $base_path . '/';
}
if (is_null($argument)) {
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
}
else {
if ($domain = drush_domain_get_from_argument($argument)) {
$domains = array($domain);
}
else {
return;
}
}
foreach ($domains as $domain) {
if ($domain->getResponse() != 200) {
drush_print(dt('Fail: !error. Please pass a --uri parameter or a --base_path to retest.' , array('!error' => $domain->getResponse())));
}
else {
drush_print(dt('Success: !url tested successfully.', array('!url' => $domain->getPath())));
}
}
}
/**
* Sets the default domain id.
*/
function drush_domain_default($argument) {
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
$validate = (drush_get_option('validate')) ? 1 : 0;
$domain->addProperty('validate_url', $validate);
if ($error = domain_drush_check_response($domain)) {
drush_set_error('domain', $error);
}
else {
$domain->saveDefault();
drush_print(dt('!domain set to primary domain.', array('!domain' => $domain->getHostname())));
}
}
}
/**
* Disables a domain.
*/
function drush_domain_disable($argument) {
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
if ($domain->status()) {
$domain->disable();
drush_print(dt('!domain has been disabled.', array('!domain' => $domain->getHostname())));
}
else {
drush_print(dt('!domain is already disabled.', array('!domain' => $domain->getHostname())));
}
}
}
/**
* Enables a domain.
*/
function drush_domain_enable($argument) {
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
if (!$domain->status()) {
$domain->enable();
drush_print(dt('!domain has been enabled.', array('!domain' => $domain->getHostname())));
}
else {
drush_print(dt('!domain is already enabled.', array('!domain' => $domain->getHostname())));
}
}
}
/**
* Changes a domain name.
*/
function drush_domain_name($argument, $name) {
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
$domain->saveProperty('name', $name);
}
}
/**
* Changes a domain machine_name.
*/
function drush_domain_machine_name($argument, $machine_name) {
$machine_name = \Drupal::service('entity_type.manager')->getStorage('domain')->createMachineName($machine_name);
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
$results = \Drupal::entityTypeManager()
->getStorage('domain')
->loadByProperties(array('machine_name' => $machine_name));
foreach ($results as $result) {
if ($result->id() == $machine_name) {
drush_print(dt('The machine_name @machine_name is being used by domain @hostname.', array('@machine_name' => $machine_name, '@hostname' => $result->getHostname())));
return;
}
}
$domain->saveProperty('id', $machine_name);
}
}
/**
* Changes a domain scheme.
*/
function drush_domain_scheme($argument) {
// Resolve the domain.
if ($domain = drush_domain_get_from_argument($argument)) {
$content = drush_choice(array(1 => dt('http'), 2 => dt('https')), dt('Select the default http scheme:'), '!value');
if (empty($content)) {
return;
}
$scheme = 'http';
if ($content == 2) {
$scheme = 'https';
}
$domain->saveProperty('scheme', $scheme);
}
}
/**
* Converts a domain string or domain_id to a $domain array.
*
* On failure, throws a drush error.
*/
function drush_domain_get_from_argument($argument) {
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->load($argument);
if (!$domain) {
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->loadByHostname($argument);
}
if (!$domain) {
drush_set_error('domain', dt('Domain record not found.'));
return NULL;
}
return $domain;
}
@@ -0,0 +1,15 @@
name: Domain
description: 'Creates domain records within a Drupal installation.'
type: module
package: Domain
# version: VERSION
# core: 8.x
dependencies:
- options
configure: domain.admin
# Information added by Drupal.org packaging script on 2017-12-19
version: '8.x-1.0-alpha11'
core: '8.x'
project: 'domain'
datestamp: 1513718589
@@ -0,0 +1,38 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Domain Access module.
*/
/**
* Implements hook_install().
*
* Installs the domain admin field on users.
*/
function domain_install() {
_domain_configure_field();
}
/**
* Implements hook_update_N().
*
* Installs the domain admin field on users.
*/
function domain_update_8001() {
_domain_configure_field();
}
/**
* Configures user form display to checkboxes widget for domain admin field.
*/
function _domain_configure_field() {
// @TODO: This function is deprecated, but using the OO syntax is causing
// test fails.
entity_get_form_display('user', 'user', 'default')
->setComponent(DOMAIN_ADMIN_FIELD, array(
'type' => 'options_buttons',
'weight' => 50,
))
->save();
}
@@ -0,0 +1,7 @@
drupal.domain:
version: VERSION
js:
js/domain.js: {}
dependencies:
- core/jquery
- core/drupal
@@ -0,0 +1,5 @@
entity.domain_add:
route_name: entity.domain.add
title: 'Add domain record'
appears_on:
- domain.admin
@@ -0,0 +1,18 @@
domain.admin:
title: Domains
route_name: domain.admin
parent: system.admin_config
description: 'Domain configuration'
weight: 0
domain.admin_list:
title: Domain records
route_name: domain.admin
parent: domain.admin
description: 'Manage the domain records used by the site.'
weight: -10
domain.settings:
title: Domain settings
route_name: domain.settings
parent: domain.admin
description: 'Domain module settings'
weight: 0
@@ -0,0 +1,8 @@
domain.admin:
title: Domain records
route_name: domain.admin
base_route: domain.admin
domain.settings:
title: Domain settings
route_name: domain.settings
base_route: domain.admin
@@ -0,0 +1,162 @@
<?php
/**
* @file
* Defines a Domain concept for use with Drupal.
*/
use Drupal\Core\Url;
use Drupal\domain\DomainInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Component\Utility\Html;
use Drupal\field\Entity\FieldStorageConfig;
/**
* The name of the node access control field.
*/
const DOMAIN_ADMIN_FIELD = 'field_domain_admin';
/**
* Entity URI callback.
*
* @param \Drupal\domain\DomainInterface $domain
* The Domain object.
*
* @return \Drupal\Core\Url
* The Domain URL.
*/
function domain_uri(DomainInterface $domain) {
return Url::fromUri($domain->getPath(), ['absolute' => TRUE]);
}
/**
* Implements hook_entity_load().
*
* The $domain->path and $domain->uri properties are derived from data in the
* {domain} table. We use the hook system to load that data to indicate that
* the data is not native to the object.
*
* This action is performed in hook_entity_load(), which precedes the running
* of hook_domain_load() and ensures that our data is present for other modules.
*/
function domain_entity_load(array $entities, $entity_type) {
if ($entity_type == 'domain') {
foreach ($entities as $domain) {
$domain->setPath();
$domain->setUrl();
}
}
}
/**
* Implements hook_help().
*/
function domain_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'domain.admin':
$output = t('<p>The following domains have been created for your site. The currently active domain
<strong>is shown in boldface</strong>. You may click on a domain to change the currently active domain.
</p>');
return $output;
}
}
/**
* Implements hook_token_info().
*/
function domain_token_info() {
return \Drupal::service('domain.token')->getTokenInfo();
}
/**
* Implements hook_tokens().
*/
function domain_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
return \Drupal::service('domain.token')->getTokens($type, $tokens, $data, $options, $bubbleable_metadata);
}
/**
* Implements hook_preprocess_HOOK() for html.html.twig.
*/
function domain_preprocess_html(array &$variables) {
// Add class to body tag, if set.
$config = \Drupal::config('domain.settings');
if ($string = $config->get('css_classes')) {
$token = \Drupal::token();
// Prepare the classes proparly, with one class per string.
$classes = explode(' ', trim($string));
foreach ($classes as $class) {
// Ensure no leading or trailing space.
$class = trim($class);
if (!empty($class)) {
$variables['attributes']['class'][] = Html::getClass($token->replace($class));
}
}
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for \Drupal\user\UserForm.
*
* Handle settings that the user cannot access.
*/
function domain_form_user_form_alter(&$form, &$form_state, $form_id) {
// Add the options hidden from the user silently to the form.
$manager = \Drupal::service('domain.element_manager');
$form = $manager->setFormOptions($form, $form_state, DOMAIN_ADMIN_FIELD);
}
/**
* Implements hook_domain_references_alter().
*/
function domain_domain_references_alter($query, $account, $context) {
// Restrict domains by assignment, being sure only to act on the admin field.
if ($context['field_type'] == 'admin' && $context['entity_type'] == 'user') {
if ($account->hasPermission('administer domains')) {
// Do nothing.
}
elseif ($account->hasPermission('assign domain administrators')) {
$allowed = \Drupal::service('domain.element_manager')->getFieldValues($account, DOMAIN_ADMIN_FIELD);
$query->condition('id', array_keys($allowed), 'IN');
}
else {
// Remove all options.
$query->condition('id', '-no-possible-match-');
}
}
}
/**
* Implements hook_views_data_alter.
*/
function domain_views_data_alter(array &$data) {
$table = 'user__' . DOMAIN_ADMIN_FIELD;
// Since domains are not stored in the database, relationships cannot be used.
unset($data[$table][DOMAIN_ADMIN_FIELD]['relationship']);
}
/**
* Implements hook_theme().
*/
function domain_theme() {
return [
'domain_nav_block' => [
'render element' => 'items',
],
];
}
/**
* Prepares variables for block templates.
*
* Default template: domain-nav-block.html.twig.
*
* @param array $variables
* An associative array containing:
* - items: An array of labels and urls for use in the list.
* Properties used: 'label', 'url', 'active'
*/
function template_preprocess_domain_nav_block(&$variables) {
$variables['items'] = $variables['items']['#items'];
}
@@ -0,0 +1,32 @@
administer domains:
title: 'Administer all domain records'
description: 'View, create, edit, and delete domain records. Allows all permissions for the module.'
restrict access: true
access inactive domains:
title: 'Access inactive domains'
description: 'Access domain URLs for domains marked as inactive.'
restrict access: true
assign domain administrators:
title: 'Assign additional administrators to assigned domains'
restrict access: true
create domains:
title: 'Create domain records'
restrict access: true
edit assigned domains:
title: 'Edit assigned domain records'
restrict access: true
delete assigned domains:
title: 'Delete assigned domain records'
restrict access: true
use domain nav block:
title: 'Access the domain navigation block'
use domain switcher block:
title: 'Access the domain switcher block'
restrict access: true
view assigned domains:
title: 'View assigned domains in the administration list'
view domain list:
title: 'View all registered domains in the administration list'
view domain information:
title: 'View debugging information for domain handling'
restrict access: true
@@ -0,0 +1,55 @@
domain.admin:
path: '/admin/config/domain'
defaults:
_entity_list: 'domain'
_title: 'Domains'
_title_context: 'With modules'
requirements:
_custom_access: 'Drupal\domain\Access\DomainListCheck::viewDomainList'
domain.settings:
path: '/admin/config/domain/settings'
defaults:
_title: 'Domain settings'
_form: '\Drupal\domain\Form\DomainSettingsForm'
requirements:
_permission: 'administer domains'
entity.domain.add:
path: '/admin/config/domain/add'
defaults:
_entity_form: domain.edit
_title: 'Add domain record'
options:
_admin_route: TRUE
requirements:
_entity_create_access: domain
entity.domain.edit_form:
path: '/admin/config/domain/edit/{domain}'
defaults:
_entity_form: domain.edit
_title: 'Edit domain record'
options:
_admin_route: TRUE
requirements:
_entity_access: domain.update
entity.domain.delete_form:
path: '/admin/config/domain/delete/{domain}'
defaults:
_entity_form: domain.delete
_title: 'Delete'
options:
_admin_route: TRUE
requirements:
_entity_access: domain.delete
domain.inline_action:
path: '/admin/config/domain/{op}/{domain}'
defaults:
_controller: 'Drupal\domain\Controller\DomainController::ajaxOperation'
requirements:
_entity_access: domain.update
_csrf_token: 'TRUE'
op: 'enable|disable|default'
@@ -0,0 +1,41 @@
services:
access_check.domain:
class: Drupal\domain\Access\DomainAccessCheck
tags:
- { name: access_check }
arguments: ['@domain.negotiator', '@config.factory', '@path.matcher']
access_check.domain_route:
class: Drupal\domain\Access\DomainRouteCheck
tags:
- { name: access_check, applies_to: _domain }
arguments: ['@domain.negotiator']
domain.current_domain_context:
class: Drupal\domain\ContextProvider\CurrentDomainContext
arguments: ['@domain.negotiator']
tags:
- { name: 'context_provider' }
domain.element_manager:
class: Drupal\domain\DomainElementManager
arguments: ['@entity_type.manager']
domain.negotiator:
class: Drupal\domain\DomainNegotiator
arguments: ['@request_stack', '@module_handler', '@entity_type.manager', '@config.factory']
domain.subscriber:
class: Drupal\domain\EventSubscriber\DomainSubscriber
tags:
- { name: event_subscriber }
arguments: ['@domain.negotiator', '@entity_type.manager', '@access_check.domain', '@current_user']
domain.token:
class: Drupal\domain\DomainToken
arguments: ['@entity_type.manager', '@domain.negotiator']
domain.validator:
class: Drupal\domain\DomainValidator
arguments: ['@module_handler', '@config.factory', '@http_client', '@entity_type.manager']
# @deprecated and will be removed for 8.x-1.0 release.
domain.creator:
class: Drupal\domain\DomainCreator
arguments: ['@domain.loader', '@domain.negotiator']
# @deprecated and will be removed for 8.x-1.0 release.
domain.loader:
class: Drupal\domain\DomainLoader
arguments: ['@config.typed', '@config.factory']
@@ -0,0 +1,42 @@
/**
* @file
* Attaches behaviors for the Domain module.
*/
(function ($) {
"use strict";
/**
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.domainSettingsSummaries = {
attach: function () {
// The drupalSetSummary method required for this behavior is not available
// on the Blocks administration page, so we need to make sure this
// behavior is processed only if drupalSetSummary is defined.
if (typeof jQuery.fn.drupalSetSummary === 'undefined') {
return;
}
// There may be an easier way to do this. Right now, we just copy code
// from block module.
function checkboxesSummary(context) {
var vals = [];
var $checkboxes = $(context).find('input[type="checkbox"]:checked + label');
var il = $checkboxes.length;
for (var i = 0; i < il; i++) {
vals.push($($checkboxes[i]).html());
}
if (!vals.length) {
vals.push(Drupal.t('Not restricted'));
}
return vals.join(', ');
}
$('[data-drupal-selector="edit-visibility-domain"]').drupalSetSummary(checkboxesSummary);
}
};
})(jQuery);
@@ -0,0 +1,17 @@
id: d7_domain
label: Domain Records
migration_tags:
- Drupal 7
source:
plugin: d7_domain
process:
id: machine_name
name: sitename
hostname: subdomain
weight: weight
is_default: is_default
scheme: scheme
path: subdomain
status: valid
destination:
plugin: entity:domain
@@ -0,0 +1,93 @@
<?php
namespace Drupal\domain\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainNegotiatorInterface;
use Symfony\Component\Routing\Route;
/**
* Provides a global access check to ensure inactive domains are restricted.
*/
class DomainAccessCheck implements AccessCheckInterface {
/**
* The Domain negotiator.
*
* @var \Drupal\domain\DomainNegotiatorInterface
*/
protected $domainNegotiator;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The path matcher service.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* Constructs the object.
*
* @param DomainNegotiatorInterface $negotiator
* The domain negotiation service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher service
*/
public function __construct(DomainNegotiatorInterface $negotiator, ConfigFactoryInterface $config_factory, PathMatcherInterface $path_matcher) {
$this->domainNegotiator = $negotiator;
$this->configFactory = $config_factory;
$this->pathMatcher = $path_matcher;
}
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return $this->checkPath($route->getPath());
}
/**
* {@inheritdoc}
*/
public function checkPath($path) {
$allowed_paths = $this->configFactory->get('domain.settings')->get('login_paths');
return !$this->pathMatcher->matchPath($path, $allowed_paths);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
/** @var \Drupal\domain\DomainInterface $domain */
$domain = $this->domainNegotiator->getActiveDomain();
// Is the domain allowed?
// No domain, let it pass.
if (empty($domain)) {
return AccessResult::allowed()->setCacheMaxAge(0);
}
// Active domain, let it pass.
if ($domain->status()) {
return AccessResult::allowed()->setCacheMaxAge(0);
}
// Inactive domain, require permissions.
else {
$permissions = array('administer domains', 'access inactive domains');
$operator = 'OR';
return AccessResult::allowedIfHasPermissions($account, $permissions, $operator)->setCacheMaxAge(0);
}
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\domain\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Custom access control handler for the domain overview page.
*/
class DomainListCheck {
/**
* Handles route permissions on the domain list page.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The account making the route request.
*
* @return \Drupal\Core\Access\AccessResult
*/
public static function viewDomainList(AccountInterface $account) {
if ($account->hasPermission('administer domains') || $account->hasPermission('view domain list') || $account->hasPermission('view assigned domains')) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
}
}
@@ -0,0 +1,82 @@
<?php
namespace Drupal\domain\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
use Drupal\domain\DomainNegotiatorInterface;
/**
* Determines access to routes based on domains.
*
* You can specify the '_domain' key on route requirements. If you specify a
* single domain, users with that domain with have access. If you specify multiple
* ones you can join them by using "+".
*
* This access checker is separate from the global check used by inactive
* domains. It is expressly for use with Views and other systems that need
* to add a domain requirement to a specific route.
*/
class DomainRouteCheck implements AccessInterface {
/**
* The key used by the routing requirement.
*
* @var string
*/
protected $requirementsKey = '_domain';
/**
* The Domain negotiator.
*
* @var \Drupal\domain\DomainNegotiatorInterface
*/
protected $domainNegotiator;
/**
* Constructs the object.
*
* @param DomainNegotiatorInterface $negotiator
* The domain negotiation service.
*/
public function __construct(DomainNegotiatorInterface $negotiator) {
$this->domainNegotiator = $negotiator;
}
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return $route->hasRequirement($this->requirementsKey);
}
/**
* Checks access to a route with a _domain requirement.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @see \Drupal\domain\Plugin\views\access\Domain
*/
public function access(Route $route, AccountInterface $account) {
// Requirements just allow strings, so this might be a comma separated list.
$string = $route->getRequirement($this->requirementsKey);
$domain = $this->domainNegotiator->getActiveDomain();
// Since only one domain can be active per request, we only suport OR logic.
$allowed = array_filter(array_map('trim', explode('+', $string)));
if (!empty($domain) && in_array($domain->id(), $allowed)) {
return AccessResult::allowed()->addCacheContexts(['url.site']);
}
// If there is no allowed domain, give other access checks a chance.
return AccessResult::neutral()->addCacheContexts(['url.site']);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\domain\ContextProvider;
use Drupal\domain\DomainNegotiatorInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a context handler for the block system.
*/
class CurrentDomainContext implements ContextProviderInterface {
use StringTranslationTrait;
/**
* The Domain negotiator.
*
* @var \Drupal\domain\DomainNegotiatorInterface
*/
protected $negotiator;
/**
* Constructs a CurrentDomainContext object.
*
* @param \Drupal\domain\DomainNegotiatorInterface $negotiator
* The domain negotiator.
*/
public function __construct(DomainNegotiatorInterface $negotiator) {
$this->negotiator = $negotiator;
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
// Load the current domain.
$current_domain = $this->negotiator->getActiveDomain();
// Set the context.
$context = new Context(new ContextDefinition('entity:domain', $this->t('Active domain')), $current_domain);
// Allow caching.
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['url.site']);
$context->addCacheableDependency($cacheability);
// Prepare the result.
$result = [
'entity:domain' => $context,
];
return $result;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\domain\Controller;
use Drupal\Core\Url;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\domain\DomainInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Controller routines for AJAX callbacks for domain actions.
*/
class DomainController {
use StringTranslationTrait;
/**
* Handles AJAX operations from the overview form.
*
* @param \Drupal\domain\DomainInterface $domain
* A domain record object.
* @param string|NULL $op
* The operation being performed, either 'default' to make the domain record
* the default, 'enable' to enable the domain record, or 'disable' to
* disable the domain record.
*
* Note: The delete action is handled by the entity form system.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response to redirect back to the domain record list.
* Supported by the UrlGeneratorTrait.
*
* @see \Drupal\domain\DomainListBuilder
*/
public function ajaxOperation(DomainInterface $domain, $op = NULL) {
$success = FALSE;
switch ($op) {
case 'default':
$domain->saveDefault();
$message = $this->t('Domain record set as default');
if ($domain->isDefault()) {
$success = TRUE;
}
break;
case 'enable':
$domain->enable();
$message = $this->t('Domain record has been enabled.');
if ($domain->status()) {
$success = TRUE;
}
break;
case 'disable':
$domain->disable();
$message = $this->t('Domain record has been disabled.');
if (!$domain->status()) {
$success = TRUE;
}
break;
}
// Set a message.
if ($success) {
drupal_set_message($message);
}
else {
drupal_set_message($this->t('The operation failed.'));
}
// Return to the invoking page.
$url = Url::fromRoute('domain.admin', array(), array('absolute' => TRUE));
return new RedirectResponse($url->toString(), 302);
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\domain\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\domain\DomainStorageInterface;
/**
* Sets a base class for injecting domain information into controllers.
*
* This class is useful in cases where your controller needs to respond to
* a domain argument. Drupal doesn't do that natively, so we use this base
* class to allow router arguments to be passed a domain object.
*
* @see \Drupal\domain_alias\Controller\DomainAliasController
*/
class DomainControllerBase extends ControllerBase {
/**
* The entity storage.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new DomainControllerBase.
*
* @param \Drupal\domain\DomainStorageInterface $domain_storage
* The storage controller.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct(DomainStorageInterface $domain_storage, EntityTypeManagerInterface $entity_type_manager) {
$this->domainStorage = $domain_storage;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('domain'),
$container->get('entity_type.manager')
);
}
}
@@ -0,0 +1,119 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainElementManagerInterface;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access controller for the domain entity type.
*
* Note that this is not a node access check.
*/
class DomainAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* The entity type manager
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The domain field element manager.
*
* @var \Drupal\domain\DomainElementManagerInterface
*/
protected $domainElementManager;
/**
* The user storage manager.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs an access control handler instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\domain\DomainElementManagerInterface $domain_element_manager
* The domain field element manager.
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, DomainElementManagerInterface $domain_element_manager, UserStorageInterface $user_storage) {
parent::__construct($entity_type);
$this->entityTypeManager = $entity_type_manager;
$this->domainElementManager = $domain_element_manager;
$this->userStorage = $user_storage;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager'),
$container->get('domain.element_manager'),
$container->get('entity_type.manager')->getStorage('user')
);
}
/**
* {@inheritdoc}
*/
public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account = NULL) {
$account = $this->prepareUser($account);
// Check the global permission.
if ($account->hasPermission('administer domains')) {
return AccessResult::allowed();
}
// @TODO: This may not be relevant.
if ($operation == 'create' && $account->hasPermission('create domains')) {
return AccessResult::allowed();
}
// For view, we allow admins unless the domain is inactive.
$is_admin = $this->isDomainAdmin($entity, $account);
if ($operation == 'view' && ($entity->status() || $account->hasPermission('access inactive domains')) && ($is_admin || $account->hasPermission('view domain list'))) {
return AccessResult::allowed();
}
// For other operations, check that the user is a domain admin.
if ($operation == 'update' && $account->hasPermission('edit assigned domains') && $is_admin) {
return AccessResult::allowed();
}
if ($operation == 'delete' && $account->hasPermission('delete assigned domains') && $is_admin) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
}
/**
* Checks if a user can administer a specific domain.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to retrieve field data from.
* @param \Drupal\Core\Session\AccountInterface
* The user account.
*
* @return boolean
*/
public function isDomainAdmin(EntityInterface $entity, AccountInterface $account) {
$user = $this->userStorage->load($account->id());
$user_domains = $this->domainElementManager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
return isset($user_domains[$entity->id()]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Drupal\domain;
/**
* Creates new domain records.
*
* This class is a helper that replaces legacy procedural code.
*
* @deprecated
* This class will be removed before the 8.1.0 release.
* Use DomainStorage instead, loaded through the EntityTypeManager.
*/
class DomainCreator implements DomainCreatorInterface {
/**
* The Domain loader.
*
* @var \Drupal\domain\DomainLoaderInterface $loader
*/
protected $loader;
/**
* The Domain negotiator.
*
* @var \Drupal\domain\DomainNegotiatorInterface $negotiator
*/
protected $negotiator;
/**
* Constructs a DomainCreator object.
*
* @param \Drupal\domain\DomainLoaderInterface $loader
* The domain loader.
* @param \Drupal\domain\DomainNegotiatorInterface $negotiator
* The domain negotiator.
*/
public function __construct(DomainLoaderInterface $loader, DomainNegotiatorInterface $negotiator) {
$this->loader = $loader;
$this->negotiator = $negotiator;
}
/**
* {@inheritdoc}
*/
public function createDomain(array $values = array()) {
$default = $this->loader->loadDefaultId();
$domains = $this->loader->loadMultiple();
if (empty($values)) {
$values['hostname'] = $this->createHostname();
$values['name'] = \Drupal::config('system.site')->get('name');
}
$values += array(
'scheme' => \Drupal::service('entity_type.manager')->getStorage('domain')->getDefaultScheme(),
'status' => 1,
'weight' => count($domains) + 1,
'is_default' => (int) empty($default),
);
$domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values);
return $domain;
}
/**
* {@inheritdoc}
*/
public function createHostname() {
return $this->negotiator->negotiateActiveHostname();
}
/**
* {@inheritdoc}
*/
public function createMachineName($hostname = NULL) {
if (empty($hostname)) {
$hostname = $this->createHostname();
}
return preg_replace('/[^a-z0-9_]/', '_', $hostname);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\domain;
/**
* Handles the creation of new domain records.
* @deprecated
* This interface will be removed before the 8.1.0 release.
*/
interface DomainCreatorInterface {
/**
* Creates a domain object for saving.
*
* @param array $values
* The values to assign to the domain record.
* Required values are: hostname, name.
* Passing an empty array will create a domain from the current request.
*
* @return DomainInterface $domain
* A domain record object.
*/
public function createDomain(array $values = array());
/**
* Gets the hostname of the active request.
*
* @return string
* The hostname string of the current request.
*/
public function createHostname();
/**
* Creates a machine-name string from the hostname.
*
* This string is the primary key of the entity.
*
* @param string $hostname
* The hostname of the domain record. If empty, the current request will be
* used.
*
* @return string
* A string containing A-Z, a-z, 0-9, and _ characters.
*/
public function createMachineName($hostname = NULL);
}
@@ -0,0 +1,204 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Generic base class for handling hidden field options.
*
* Since domain options are restricted for various forms (users, nodes, source)
* we have a base class for handling common use cases. The details of each
* implementation are generally handled by a subclass and invoked within a
* hook_form_alter().
*
* This class has some similarities to DomainAccessManager, but only cares
* about form handling. It can be used as a base class by other modules that
* show/hide domain options. See the DomainSourceElementManager for a non-default
* implementation.
*/
class DomainElementManager implements DomainElementManagerInterface {
use StringTranslationTrait;
/**
* The entity type manager
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* Constructs a DomainElementManager object.
*
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->domainStorage = $entity_type_manager->getStorage('domain');
}
/**
* @inheritdoc
*/
public function setFormOptions(array $form, FormStateInterface $form_state, $field_name, $hide_on_disallow = FALSE) {
// There are cases, such as Entity Browser, where the form is partially
// invoked, but without our fields.
if (!isset($form[$field_name])) {
return $form;
}
$fields = $this->fieldList($field_name);
$disallowed = $this->disallowedOptions($form_state, $form[$field_name]);
$empty = empty($form[$field_name]['widget']['#options']);
// If the domain form element is set as a group, and the field is not assigned to
// another group, then move it. See domain_access_form_node_form_alter().
if (isset($form['domain']) && !isset($form[$field_name]['#group'])) {
$form[$field_name]['#group'] = 'domain';
}
// Check for domains the user cannot access or the absence of any options.
if (!empty($disallowed) || $empty) {
// @TODO: Potentially show this information to users with permission.
$form[$field_name . '_disallowed'] = array(
'#type' => 'value',
'#value' => $disallowed,
);
$form['domain_hidden_fields'] = array(
'#type' => 'value',
'#value' => $fields,
);
if ($hide_on_disallow || $empty) {
$form[$field_name]['#access'] = FALSE;
}
elseif (!empty($disallowed)) {
$form[$field_name]['widget']['#description'] .= $this->listDisallowed($disallowed);
}
// Call our submit function to merge in values.
// Account for all the submit buttons on the node form.
$buttons = ['preview', 'delete'];
$submit = $this->getSubmitHandler();
foreach ($form['actions'] as $key => $action) {
if (!in_array($key, $buttons) && isset($form['actions'][$key]['#submit']) && !in_array($submit, $form['actions'][$key]['#submit'])) {
array_unshift($form['actions'][$key]['#submit'], $submit);
}
}
}
return $form;
}
/**
* @inheritdoc
*/
public static function submitEntityForm(array &$form, FormStateInterface $form_state) {
$fields = $form_state->getValue('domain_hidden_fields');
foreach ($fields as $field) {
$entity_values = [];
$values = $form_state->getValue($field . '_disallowed');
if (!empty($values)) {
$info = $form_state->getBuildInfo();
$node = $form_state->getFormObject()->getEntity();
$entity_values = $form_state->getValue($field);
}
foreach ($values as $value) {
$entity_values[]['target_id'] = $value;
}
// Prevent a fatal error caused by passing a NULL value.
// See https://www.drupal.org/node/2841962.
if (!empty($entity_values)) {
$form_state->setValue($field, $entity_values);
}
}
}
/**
* @inheritdoc
*/
public function disallowedOptions(FormStateInterface $form_state, $field) {
$options = [];
$info = $form_state->getBuildInfo();
$entity = $form_state->getFormObject()->getEntity();
$entity_values = $this->getFieldValues($entity, $field['widget']['#field_name']);
if (isset($field['widget']['#options'])) {
$options = array_diff_key($entity_values, $field['widget']['#options']);
}
return array_keys($options);
}
/**
* @inheritdoc
*/
public function fieldList($field_name) {
static $fields = [];
$fields[] = $field_name;
// Return only unique field names. AJAX requests can result in duplicates.
// See https://www.drupal.org/project/domain/issues/2930934.
return array_unique($fields);
}
/**
* @inheritdoc
*/
public function getFieldValues($entity, $field_name) {
// @TODO: static cache.
$list = [];
// @TODO In tests, $entity is returning NULL.
if (is_null($entity)) {
return $list;
}
// Get the values of an entity.
$values = $entity->hasField($field_name) ? $entity->get($field_name) : NULL;
// Must be at least one item.
if (!empty($values)) {
foreach ($values as $item) {
if ($target = $item->getValue()) {
if ($domain = $this->domainStorage->load($target['target_id'])) {
$list[$domain->id()] = $domain->getDomainId();
}
}
}
}
return $list;
}
/**
* @inheritdoc
*/
public function getSubmitHandler() {
return '\\Drupal\\domain\\DomainElementManager::submitEntityForm';
}
/**
* Lists the disallowed domains in the user interface.
*
* @param array $disallowed
* An array of domain ids.
*
* @return string
* A string suitable for display.
*/
public function listDisallowed(array $disallowed) {
$domains = $this->domainStorage->loadMultiple($disallowed);
// @TODO: Proper theme function here.
$string = $this->t('The following domains are currently assigned and cannot be changed:');
foreach ($domains as $domain) {
$items[] = $domain->label();
}
$build = array(
'#theme' => 'item_list',
'#items' => $items,
);
$string .= render($build);
return '<div class="disallowed">' . $string . '</div>';
}
}
@@ -0,0 +1,105 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Form\FormStateInterface;
/**
* Handles hidden field options for domain entity references.
*
* Since domain options are restricted for various forms (users, nodes, source)
* we have a base class for handling common use cases. The details of each
* implementation are generally handled by a subclass and invoked within a
* hook_form_alter().
*
* The default implementation, DomainElementManger, works with standard
* multi-value domain entity reference fields.
*/
interface DomainElementManagerInterface {
/**
* Resets form options and stores hidden values that the user cannot change.
*
* @param array $form
* The form array.
* @param FormStateInterface $form_state
* The form state object.
* @param $field_name
* The name of the field to check.
* @param boolean $hide_on_disallow
* If the field is set to a value that cannot be altered by the user who
* is not assigned to that domain, pass TRUE to remove the form element
* entirely. See DomainSourceElementManager for the use-case.
*
* @return array $form
* Return the modified form array.
*/
public function setFormOptions(array $form, FormStateInterface $form_state, $field_name, $hide_on_disallow = FALSE);
/**
* Submit function for handling hidden values from a form.
*
* On form submit, loop through the hidden form values and add those to the
* entity being saved.
*
* @param $form
* The form array.
* @param Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return
* No return value. Hidden values are added to the field values directly.
*/
public static function submitEntityForm(array &$form, FormStateInterface $form_state);
/**
* Finds options not accessible to the current user.
*
* @param Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param array $field
* The field element being processed.
*/
public function disallowedOptions(FormStateInterface $form_state, $field);
/**
* Stores a static list of fields that have been disallowed.
*
* @param $field_name
* The name of the field being processed. Inherited from setFormOptions.
*
* @return array
* An array of field names.
*/
public function fieldList($field_name);
/**
* Gets the domain entity reference field values from an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to retrieve field data from.
* @param string $field_name
* The name of the field that holds our data.
*
* @return array
* The domain access field values, keyed by id (machine_name) with value of
* the numeric domain_id used by node access.
*/
public function getFieldValues($entity, $field_name);
/**
* Returns the default submit handler to be used for a field element.
*
* @return string
* A fully-qualified class and method name, such as
* '\\Drupal\\domain\\DomainElementManager::submitEntityForm'
*
* The method must be public and static, since it will be called from the
* form submit handler without knowledge of the parent class.
*
* The base implementat is submitEntityForm, and can be overridden by
* specific subclasses.
*/
public function getSubmitHandler();
}
@@ -0,0 +1,218 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\ConfigValueException;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\domain\DomainStorageInterface;
/**
* Base form for domain edit forms.
*/
class DomainForm extends EntityForm {
/**
* The domain entity storage.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The domain validator.
*
* @var \Drupal\domain\DomainValidatorInterface
*/
protected $validator;
/**
* The entity type manager
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a DomainForm object.
*
* @param \Drupal\domain\DomainStorageInterface $domain_storage
* The domain storage manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\domain\DomainValidatorInterface $validator
* The domain validator.
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(DomainStorageInterface $domain_storage, RendererInterface $renderer, DomainValidatorInterface $validator, EntityTypeManagerInterface $entity_type_manager) {
$this->domainStorage = $domain_storage;
$this->renderer = $renderer;
$this->validator = $validator;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('domain'),
$container->get('renderer'),
$container->get('domain.validator'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\domain\Entity\Domain $domain */
$domain = $this->entity;
// Create defaults if this is the first domain.
$count_existing = $this->domainStorage->getQuery()->count()->execute();
if (!$count_existing) {
$domain->addProperty('hostname', $this->domainStorage->createHostname());
$domain->addProperty('name', $this->config('system.site')->get('name'));
}
$form['domain_id'] = array(
'#type' => 'value',
'#value' => $domain->getDomainId(),
);
$form['hostname'] = array(
'#type' => 'textfield',
'#title' => $this->t('Hostname'),
'#size' => 40,
'#maxlength' => 80,
'#default_value' => $domain->getCanonical(),
'#description' => $this->t('The canonical hostname, using the full <em>subdomain.example.com</em> format. Leave off the http:// and the trailing slash and do not include any paths.<br />If this domain uses a custom http(s) port, you should specify it here, e.g.: <em>subdomain.example.com:1234</em><br />The hostname may contain only lowercase alphanumeric characters, dots, dashes, and a colon (if using alternative ports).'),
);
$form['id'] = array(
'#type' => 'machine_name',
'#default_value' => !empty($domain->id()) ? $domain->id() : '',
'#disabled' => !empty($domain->id()),
'#machine_name' => array(
'source' => array('hostname'),
'exists' => array($this->domainStorage, 'load'),
),
);
$form['name'] = array(
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#size' => 40,
'#maxlength' => 80,
'#default_value' => $domain->label(),
'#description' => $this->t('The human-readable name is shown in domain lists and may be used as the title tag.'),
);
// Do not use the :// suffix when storing data.
$add_suffix = FALSE;
$form['scheme'] = array(
'#type' => 'radios',
'#title' => $this->t('Domain URL scheme'),
'#options' => array('http' => 'http://', 'https' => 'https://', 'variable' => 'Variable'),
'#default_value' => $domain->getRawScheme(),
'#description' => $this->t('This URL scheme will be used when writing links and redirects to this domain and its resources. Selecting <strong>Variable</strong> will inherit the current scheme of the web request.'),
);
$form['status'] = array(
'#type' => 'radios',
'#title' => $this->t('Domain status'),
'#options' => array(1 => $this->t('Active'), 0 => $this->t('Inactive')),
'#default_value' => (int) $domain->status(),
'#description' => $this->t('"Inactive" domains are only accessible to user roles with that assigned permission.'),
);
$form['weight'] = array(
'#type' => 'weight',
'#title' => $this->t('Weight'),
'#delta' => $count_existing + 1,
'#default_value' => $domain->getWeight(),
'#description' => $this->t('The sort order for this record. Lower values display first.'),
);
$form['is_default'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Default domain'),
'#default_value' => $domain->isDefault(),
'#description' => $this->t('If a URL request fails to match a domain record, the settings for this domain will be used. Only one domain can be default.'),
);
$form['validate_url'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Test server response'),
'#default_value' => TRUE,
'#description' => $this->t('Validate that url of the host is accessible to Drupal before saving.'),
);
$required = $this->validator->getRequiredFields();
foreach ($form as $key => $element) {
if (in_array($key, $required)) {
$form[$key]['#required'] = TRUE;
}
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\domain\DomainInterface $entity */
$entity = $this->entity;
$hostname = $entity->getHostname();
$errors = $this->validator->validate($hostname);
if (!empty($errors)) {
// Render errors to display as message.
$message = [
'#theme' => 'item_list',
'#items' => $errors,
];
$message = $this->renderer->renderPlain($message);
$form_state->setErrorByName('hostname', $message);
}
// Validate if the same hostname exists.
// Do not use domain loader because it may change hostname.
$existing = $this->domainStorage->loadByProperties(['hostname' => $hostname]);
$existing = reset($existing);
// If we have already registered a hostname, make sure we don't create a duplicate.
// We cannot check id() here, as the machine name is editable.
if ($existing && $existing->getDomainId() != $entity->getDomainId()) {
$form_state->setErrorByName('hostname', $this->t('The hostname is already registered.'));
}
// Check the domain response. First, clear the path value.
$entity->setPath();
// Check the response.
$response = $this->validator->checkResponse($entity);
// If validate_url is set, then we must receive a 200 response.
if ($entity->validate_url && $response != 200) {
if (empty($response)) {
$response = 500;
}
$form_state->setErrorByName('hostname', $this->t('The server request to @url returned a @response response. To proceed, disable the <em>Test server response</em> in the form.', ['@url' => $entity->getPath(), '@response' => $response]));
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
if ($status == SAVED_NEW) {
drupal_set_message($this->t('Domain record created.'));
}
else {
drupal_set_message($this->t('Domain record updated.'));
}
$form_state->setRedirect('domain.admin');
}
}
@@ -0,0 +1,241 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a domain entity.
*/
interface DomainInterface extends ConfigEntityInterface {
/**
* Detects if the current domain is the active domain.
*
* @return bool
* TRUE if domain enabled, FALSE otherwise.
*/
public function isActive();
/**
* Detects if the current domain is the default domain.
*
* @return bool
* TRUE if domain set as default, FALSE otherwise.
*/
public function isDefault();
/**
* Detects if the domain uses https for links.
*
* @return bool
* TRUE if domain protocol is HTTPS, FALSE otherwise.
*/
public function isHttps();
/**
* Makes a domain record the default.
*/
public function saveDefault();
/**
* Saves a specific domain attribute.
*
* @param string $name
* The property key to save for the $domain object.
* @param mixed $value
* The value to set for the property.
*/
public function saveProperty($name, $value);
/**
* Sets the base path to this domain.
*/
public function setPath();
/**
* Sets the domain-specific link to the current URL.
*/
public function setUrl();
/**
* Gets the path for a domain.
*
* @return string
* A URL string for the base path to the domain. (e.g. http://example.com/)
*/
public function getPath();
/**
* Gets the url for a domain.
*
* @return string
* A URL string for the current request on the requested domain. If the
* current request is /user the return would be http://example.com/user or
* http://one.example.com, depending on the current domain context.
*/
public function getUrl();
/**
* Returns the active scheme for a domain record.
*
* This method is to be used when generating URLs.
*
* @param bool $add_suffix
* Tells the method to return :// after the string.
*
* @return string
* Returns a valid scheme (http or https), with or without the suffix.
*/
public function getScheme($add_suffix = TRUE);
/**
* Returns the stored scheme value for a domain record.
*
* This method is to be used with forms and when saving domain records. It
* returns the raw value (http|https|variable) of the domain's default scheme.
*
* @return string
* Returns a stored scheme default (http|https|variable) for the record.
*/
public function getRawScheme();
/**
* Retrieves the value of the response test.
*
* @return int
* The HTTP response code of the domain test, expected to be 200.
*/
public function getResponse();
/**
* Sets the value of the response test.
*
* @param int $response
* The HTTP response code to set.
*/
public function setResponse($response);
/**
* Adds a property to the domain record.
*
* @param string $name
* The name of the property to retrieve.
* @param mixed $value
* The value of the property.
*/
public function addProperty($name, $value);
/**
* Returns a URL object for a domain.
*
* @param bool $current_path
* Indicates that the link should point to the path of the current request.
*
* @return \Drupal\Core\Url
* A core URL object.
*/
public function getLink($current_path = TRUE);
/**
* Returns the redirect status of the current domain.
*
* @return int|null
* If numeric, the type of redirect to issue (301 or 302).
*/
public function getRedirect();
/**
* Sets a redirect on the current domain.
*
* @param int $code
* A valid HTTP redirect code (301 or 302).
*/
public function setRedirect($code = 302);
/**
* Gets the hostname of the domain.
*
* @return string
* The domain hostname.
*/
public function getHostname();
/**
* Sets the hostname of the domain.
*
* @param string $hostname
* The hostname value to set, in the format example.com.
*/
public function setHostname($hostname);
/**
* Gets the numeric id of the domain record.
*
* @return int
* The domain id.
*/
public function getDomainId();
/**
* Gets the sort weight of the domain record.
*
* @return int
* The domain record sort weight.
*/
public function getWeight();
/**
* Sets the type of record match returned by the negotiator.
*
* @param int $match_type
* A numeric constant indicating the type of match derived by the caller.
* Use this value to determine if the request needs to be overridden. Valid
* types are DomainNegotiator::DOMAIN_MATCH_NONE,
* DomainNegotiator::DOMAIN_MATCH_EXACT,
* DomainNegotiator::DOMAIN_MATCH_ALIAS.
*/
public function setMatchType($match_type = DomainNegotiator::DOMAIN_MATCH_EXACT);
/**
* Gets the type of record match returned by the negotiator.
*
* This value will be set by the domain negotiation routine and is not present
* when loading a domain record via DomainStorageInterface.
*
* @return int
* The domain record match type.
*
* @see setMatchType()
*/
public function getMatchType();
/**
* Find the port used for the domain.
*
* @param \Drupal\domain\DomainInterface $domain
* A domain entity.
*
* @return An optional port string (e.g. ':8080') or an empty string;
*/
public function getPort();
/**
* Creates a unique domain id for this record.
*/
public function createDomainId();
/**
* Retrieves the canonical (registered) hostname for the domaim.
*
* @return string
* A hostname string.
*/
public function getCanonical();
/**
* Sets the canonical (registered) hostname for the domaim.
*/
public function setCanonical($hostname = NULL);
}
@@ -0,0 +1,347 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\domain\DomainAccessControlHandler;
use Drupal\domain\DomainStorageInterface;
use Drupal\domain\DomainElementManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* User interface for the domain overview screen.
*/
class DomainListBuilder extends DraggableListBuilder {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'domains';
/**
* The current user object.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The redirect destination helper.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
protected $destinationHandler;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The domain entity access control handler.
*
* @var \Drupal\domain\DomainAccessControlHandler
*/
protected $accessHandler;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The Domain storage handler.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* The number of entities to list per page.
*
* DraggableListBuilder sets this to FALSE, which cancels any pagination. Restore the
* default value from EntityListBuilder.
*
* @var int|false
*/
protected $limit = 50;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('current_user'),
$container->get('redirect.destination'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('domain.element_manager')
);
}
/**
* Constructs a new EntityListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\domain\DomainStorageInterface $domain_storage
* The domain storage class.
* @param \Drupal\Core\Session\AccountInterface $account
* The active user account.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $destination
* The redirect destination helper.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\domain\DomainElementManager $domain_element_manager
* The domain field element manager.
*/
public function __construct(EntityTypeInterface $entity_type, DomainStorageInterface $domain_storage, AccountInterface $account, RedirectDestinationInterface $destination_handler, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, DomainElementManager $domain_element_manager) {
parent::__construct($entity_type, $domain_storage);
$this->entityTypeId = $entity_type->id();
$this->domainStorage = $domain_storage;
$this->entityType = $entity_type;
$this->currentUser = $account;
$this->destinationHandler = $destination_handler;
$this->entityTypeManager = $entity_type_manager;
$this->accessHandler = $this->entityTypeManager->getAccessControlHandler('domain');
$this->moduleHandler = $module_handler;
$this->domainElementManager = $domain_element_manager;
$this->userStorage = $this->entityTypeManager->getStorage('user');
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'domain_admin_overview_form';
}
/**
* {@inheritdoc}
*/
public function getOperations(EntityInterface $entity) {
$operations = parent::getOperations($entity);
$destination = $this->destinationHandler->getAsArray();
$default = $entity->isDefault();
$id = $entity->id();
// If the user cannot edit domains, none of these actions are permitted.
$access = $this->accessHandler->checkAccess($entity, 'update');
if ($access->isForbidden()) {
return $operations;
}
$super_admin = $this->currentUser->hasPermission('administer domains');
if ($super_admin || $this->currentUser->hasPermission('access inactive domains')) {
if ($entity->status() && !$default) {
$operations['disable'] = array(
'title' => $this->t('Disable'),
'url' => Url::fromRoute('domain.inline_action', array('op' => 'disable', 'domain' => $id)),
'weight' => 50,
);
}
elseif (!$default) {
$operations['enable'] = array(
'title' => $this->t('Enable'),
'url' => Url::fromRoute('domain.inline_action', array('op' => 'enable', 'domain' => $id)),
'weight' => 40,
);
}
}
if (!$default && $super_admin) {
$operations['default'] = array(
'title' => $this->t('Make default'),
'url' => Url::fromRoute('domain.inline_action', array('op' => 'default', 'domain' => $id)),
'weight' => 30,
);
}
if (!$default && $this->accessHandler->checkAccess($entity, 'delete')->isAllowed()) {
$operations['delete'] = array(
'title' => $this->t('Delete'),
'url' => Url::fromRoute('entity.domain.delete_form', array('domain' => $id)),
'weight' => 20,
);
}
$operations += $this->moduleHandler->invokeAll('domain_operations', array($entity, $this->currentUser));
foreach ($operations as $key => $value) {
if (isset($value['query']['token'])) {
$operations[$key]['query'] += $destination;
}
}
/** @var DomainInterface $default */
$default = $this->domainStorage->loadDefaultDomain();
// Deleting the site default domain is not allowed.
if ($default && $id == $default->id()) {
unset($operations['delete']);
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Name');
$header['hostname'] = $this->t('Hostname');
$header['status'] = $this->t('Status');
$header['is_default'] = $this->t('Default');
$header += parent::buildHeader();
if (!$this->currentUser->hasPermission('administer domains')) {
unset($header['weight']);
}
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
// If the user cannot view the domain, none of these actions are permitted.
$admin = $this->accessHandler->checkAccess($entity, 'view');
if ($admin->isForbidden()) {
return;
}
$row['label'] = $this->getLabel($entity);
$row['hostname'] = array('#markup' => $entity->getLink());
if ($entity->isActive()) {
$row['hostname']['#prefix'] = '<strong>';
$row['hostname']['#suffix'] = '</strong>';
}
$row['status'] = array('#markup' => $entity->status() ? $this->t('Active') : $this->t('Inactive'));
$row['is_default'] = array('#markup' => ($entity->isDefault() ? $this->t('Yes') : $this->t('No')));
$row += parent::buildRow($entity);
if (!$this->currentUser->hasPermission('administer domains')) {
unset($row['weight']);
}
return $row;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form[$this->entitiesKey]['#domains'] = $this->entities;
$form['actions']['submit']['#value'] = $this->t('Save configuration');
// Only super-admins may sort domains.
if (!$this->currentUser->hasPermission('administer domains')) {
$form['actions']['submit']['#access'] = FALSE;
unset($form['#tabledrag']);
}
// Delta is set after each row is loaded.
$count = count($this->domainStorage->loadMultiple()) + 1;
foreach (Element::children($form['domains']) as $key) {
if (isset($form['domains'][$key]['weight'])) {
$form['domains'][$key]['weight']['#delta'] = $count;
}
}
return $form;
}
/**
* {@inheritdoc}
*
* Overrides the parent method to prevent saving bad data.
*
* @link https://www.drupal.org/project/domain/issues/2925798
* @link https://www.drupal.org/project/domain/issues/2925629
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $id => $value) {
if (isset($this->entities[$id]) && $this->entities[$id]->get($this->weightKey) != $value['weight']) {
// Reset weight properly.
$this->entities[$id]->set($this->weightKey, $value['weight']);
// Do not allow accidental hostname rewrites.
$this->entities[$id]->set('hostname', $this->entities[$id]->getCanonical());
$this->entities[$id]->save();
}
}
}
/**
* Internal sort method for form weights.
*/
private function sortByWeight($a, $b) {
if ($a['weight'] < $b['weight']) {
return 0;
}
return 1;
}
/**
* {@inheritdoc}
*
* Builds the entity listing as a form with pagination. This method overrides both
* Drupal\Core\Config\Entity\DraggableListBuilder::render() and
* Drupal\Core\Entity\EntityListBuilder::render().
*/
public function render() {
// Build the default form, which includes weights.
$form = $this->formBuilder()->getForm($this);
// Only add the pager if a limit is specified.
if ($this->limit) {
$form['pager'] = array(
'#type' => 'pager',
);
}
return $form;
}
/**
* {@inheritdoc}
*
* Loads entity IDs using a pager sorted by the entity weight. The default behavior when
* using a limit is to sort by id.
*
* We also have to limit by assigned domains of the active user.
*
* See Drupal\Core\Entity\EntityListBuilder::getEntityIds()
*
* @return array
* An array of entity IDs.
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery()
->sort($this->entityType->getKey('weight'));
// If the user cannot administer domains, we must filter the query further by
// assigned IDs. We don't have to check permissions here, because that is handled by
// the route system and buildRow(). There are two permissions that allow users to view
// the entire list.
if (!$this->currentUser->hasPermission('administer domains') && !$this->currentUser->hasPermission('view domain list')) {
$user = $this->userStorage->load($this->currentUser->id());
$allowed = $this->domainElementManager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
$query->condition('id', array_keys($allowed), 'IN');
}
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
}
@@ -0,0 +1,162 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
/**
* Loads Domain records.
* @deprecated
* This class will be removed before the 8.1.0 release.
* Use DomainStorage instead, loaded through the EntityTypeManager.
*/
class DomainLoader implements DomainLoaderInterface {
/**
* The typed config handler.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a DomainLoader object.
*
* Trying to inject the storage manager throws an exception.
*
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed config handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*
* @see getStorage()
*/
public function __construct(TypedConfigManagerInterface $typed_config, ConfigFactoryInterface $config_factory) {
$this->typedConfig = $typed_config;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function loadSchema() {
$fields = $this->typedConfig->getDefinition('domain.record.*');
return isset($fields['mapping']) ? $fields['mapping'] : array();
}
/**
* {@inheritdoc}
*/
public function load($id, $reset = FALSE) {
$controller = $this->getStorage();
if ($reset) {
$controller->resetCache(array($id));
}
return $controller->load($id);
}
/**
* {@inheritdoc}
*/
public function loadDefaultId() {
$result = $this->loadDefaultDomain();
if (!empty($result)) {
return $result->id();
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadDefaultDomain() {
$result = $this->getStorage()->loadByProperties(array('is_default' => TRUE));
if (!empty($result)) {
return current($result);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadMultiple(array $ids = NULL, $reset = FALSE) {
$controller = $this->getStorage();
if ($reset) {
$controller->resetCache($ids);
}
return $controller->loadMultiple($ids);
}
/**
* {@inheritdoc}
*/
public function loadMultipleSorted(array $ids = NULL) {
$domains = $this->loadMultiple($ids);
uasort($domains, array($this, 'sort'));
return $domains;
}
/**
* {@inheritdoc}
*/
public function loadByHostname($hostname) {
$hostname = $this->prepareHostname($hostname);
$result = $this->getStorage()->loadByProperties(array('hostname' => $hostname));
if (empty($result)) {
return NULL;
}
return current($result);
}
/**
* {@inheritdoc}
*/
public function loadOptionsList() {
$list = array();
foreach ($this->loadMultipleSorted() as $id => $domain) {
$list[$id] = $domain->label();
}
return $list;
}
/**
* {@inheritdoc}
*/
public function sort(DomainInterface $a, DomainInterface $b) {
return $a->getWeight() > $b->getWeight();
}
/**
* Loads the storage controller.
*
* We use the loader very early in the request cycle. As a result, if we try
* to inject the storage container, we hit a circular dependency. Using this
* method at least keeps our code easier to update.
*/
protected function getStorage() {
$storage = \Drupal::entityTypeManager()->getStorage('domain');
return $storage;
}
/**
* {@inheritdoc}
*/
public function prepareHostname($hostname) {
// Strip www. prefix off the hostname.
$ignore_www = $this->configFactory->get('domain.settings')->get('www_prefix');
if ($ignore_www && substr($hostname, 0, 4) == 'www.') {
$hostname = substr($hostname, 4);
}
return $hostname;
}
}
@@ -0,0 +1,118 @@
<?php
namespace Drupal\domain;
/**
* Supplies loader methods for common domain requests.
* @deprecated
* This interface will be removed before the 8.1.0 release.
*/
interface DomainLoaderInterface {
/**
* Loads a single domains.
*
* @param int $id
* A domain id to load.
* @param bool $reset
* Indicates that the entity cache should be reset.
*
* @return \Drupal\domain\DomainInterface|null
* A domain record or NULL.
*/
public function load($id, $reset = FALSE);
/**
* Gets the default domain object.
*
* @return \Drupal\domain\DomainInterface|null
* The default domain record or NULL.
*/
public function loadDefaultDomain();
/**
* Returns the id of the default domain.
*
* @return int
* The id of the default domain or FALSE if none is set.
*/
public function loadDefaultId();
/**
* Loads multiple domains.
*
* @param array $ids
* An optional array of specific ids to load.
* @param bool $reset
* Indicates that the entity cache should be reset.
*
* @return \Drupal\domain\DomainInterface[]
* An array of domain records.
*/
public function loadMultiple(array $ids = NULL, $reset = FALSE);
/**
* Loads multiple domains and sorts by weight.
*
* @param array $ids
* An optional array of specific ids to load.
*
* @return \Drupal\domain\DomainInterface[]
* An array of domain records.
*/
public function loadMultipleSorted(array $ids = NULL);
/**
* Loads a domain record by hostname lookup.
*
* @param string $hostname
* A hostname string, in the format example.com.
*
* @return \Drupal\domain\DomainInterface|null
* The domain record or NULL.
*/
public function loadByHostname($hostname);
/**
* Returns the list of domains formatted for a form options list.
*
* @return array
* A weight-sorted array of id => label for use in forms.
*/
public function loadOptionsList();
/**
* Sorts domains by weight.
*
* For use by loadMultipleSorted().
*
* @param DomainInterface $a
* The first Domain object to sort.
* @param DomainInterface $b
* The Domain object to compare against.
*
* @return bool
* Wether the first domain weight is greater or not.
*/
public function sort(DomainInterface $a, DomainInterface $b);
/**
* Gets the entity field schema for domain records.
*
* @return array
* An array representing the field schema of the object.
*/
public function loadSchema();
/**
* Removes www. prefix from a hostname, if set.
*
* @param string $hostname
* A hostname.
*
* @return string
* The cleaned hostname.
*/
public function prepareHostname($hostname);
}
@@ -0,0 +1,223 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* {@inheritdoc}
*/
class DomainNegotiator implements DomainNegotiatorInterface {
/**
* Defines record matching types when dealing with request alteration.
*
* @see hook_domain_request_alter().
*/
const DOMAIN_MATCH_NONE = 0;
const DOMAIN_MATCH_EXACT = 1;
const DOMAIN_MATCH_ALIAS = 2;
/**
* The HTTP_HOST value of the request.
*/
protected $httpHost;
/**
* The domain record returned by the lookup request.
*
* @var \Drupal\domain\DomainInterface
*/
protected $domain;
/**
* The domain storage class.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The request stack object.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a DomainNegotiator object.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack object.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(RequestStack $requestStack, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory) {
$this->requestStack = $requestStack;
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->domainStorage = $this->entityTypeManager->getStorage('domain');
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function setRequestDomain($httpHost, $reset = FALSE) {
// @TODO: Investigate caching methods.
$this->setHttpHost($httpHost);
// Try to load a direct match.
if ($domain = $this->domainStorage->loadByHostname($httpHost)) {
// If the load worked, set an exact match flag for the hook.
$domain->setMatchType(self::DOMAIN_MATCH_EXACT);
}
// If a straight load fails, create a base domain for checking. This data
// is required for hook_domain_request_alter().
else {
$values = array('hostname' => $httpHost);
/** @var \Drupal\domain\Entity\DomainInterface $domain */
$domain = $this->domainStorage->create($values);
$domain->setMatchType(self::DOMAIN_MATCH_NONE);
}
// Make sure all modules are loaded and can alter the found domains.
// See https://www.drupal.org/node/2896434#comment-12267208.
$this->moduleHandler->reload();
foreach ($this->moduleHandler->getImplementations('domain_request_alter') as $module) {
$this->moduleHandler->load($module);
}
// Now check with modules (like Domain Alias) that register alternate
// lookup systems with the main module.
$this->moduleHandler->alter('domain_request', $domain);
// We must have registered a valid id, else the request made no match.
if (!empty($domain->id())) {
$this->setActiveDomain($domain);
}
// Fallback to default domain if no match.
elseif ($domain = $this->domainStorage->loadDefaultDomain()) {
$this->moduleHandler->alter('domain_request', $domain);
$domain->setMatchType(self::DOMAIN_MATCH_NONE);
if (!empty($domain->id())) {
$this->setActiveDomain($domain);
}
}
}
/**
* {@inheritdoc}
*/
public function setActiveDomain(DomainInterface $domain) {
// @TODO: caching
$this->domain = $domain;
}
/**
* Determine the active domain.
*/
protected function negotiateActiveDomain() {
$httpHost = $this->negotiateActiveHostname();
$this->setRequestDomain($httpHost);
return $this->domain;
}
/**
* {@inheritdoc}
*/
public function getActiveDomain($reset = FALSE) {
if ($reset) {
$this->negotiateActiveDomain();
}
return $this->domain;
}
/**
* {@inheritdoc}
*/
public function getActiveId() {
return $this->domain->id();
}
/**
* {@inheritdoc}
*/
public function negotiateActiveHostname() {
if ($request = $this->requestStack->getCurrentRequest()) {
$httpHost = $request->getHttpHost();
}
else {
$httpHost = $_SERVER['HTTP_HOST'];
}
$hostname = !empty($httpHost) ? $httpHost : 'localhost';
return $this->domainStorage->prepareHostname($hostname);
}
/**
* {@inheritdoc}
*/
public function setHttpHost($httpHost) {
$this->httpHost = $httpHost;
}
/**
* {@inheritdoc}
*/
public function getHttpHost() {
return $this->httpHost;
}
/**
* {@inheritdoc}
*/
public function isRegisteredDomain($hostname) {
// Direct hostname match always passes.
if ($domain = $this->domainStorage->loadByHostname($hostname)) {
return TRUE;
}
// Check for registered alias matches.
$values = array('hostname' => $httpHost);
/** @var \Drupal\domain\Entity\DomainInterface $domain */
$domain = $this->domainStorage->create($values);
$domain->setMatchType(self::DOMAIN_MATCH_NONE);
// Now check with modules (like Domain Alias) that register alternate
// lookup systems with the main module.
$this->moduleHandler->alter('domain_request', $domain);
// We must have registered a valid id, else the request made no match.
if (!empty($domain->id())) {
return TRUE;
}
return FALSE;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\domain;
/**
* Handles the negotiation of the active domain record.
*/
interface DomainNegotiatorInterface {
/**
* Determines the active domain request.
*
* The negotiator is passed an httpHost value, which is checked against domain
* records for a match.
*
* @param string $httpHost
* A string representing the hostname of the request (e.g. example.com).
* @param bool $reset
* Indicates whether to reset the internal cache.
*/
public function setRequestDomain($httpHost, $reset = FALSE);
/**
* Sets the active domain.
*
* @param \Drupal\domain\DomainInterface $domain
* Sets the domain record as active for the duration of that request.
*/
public function setActiveDomain(DomainInterface $domain);
/**
* Stores the inbound httpHost request.
*
* @param string $httpHost
* A string representing the hostname of the request (e.g. example.com).
*/
public function setHttpHost($httpHost);
/**
* Gets the inbound httpHost request.
*
* @return string
* A string representing the hostname of the request (e.g. example.com).
*/
public function getHttpHost();
/**
* Gets the id of the active domain.
*
* @return string
* The id of the active domain.
*/
public function getActiveId();
/**
* Sets the hostname of the active request.
*
* This method is an internal method for use by the public getActiveDomain()
* call. It is responsible for determining the active hostname of the request
* and then passing that data to the negotiator.
*
* @return string
* The hostname, without the "www" if applicable.
*/
public function negotiateActiveHostname();
/**
* Gets the active domain.
*
* This method should be called by external classes using the negotiator
* service.
*
* @param bool $reset
* Reset the internal cache of the active domain.
*
* @return \Drupal\domain\DomainInterface
* The active domain object.
*/
public function getActiveDomain($reset = FALSE);
/**
* Checks that a URL's hostname is registered as a valid domain or alias.
*
* @param string $hostname
* A string representing the hostname of the request (e.g. example.com).
*
* @return boolean
*/
public function isRegisteredDomain($hostname);
}
@@ -0,0 +1,180 @@
<?php
namespace Drupal\domain;
use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheableResponseTrait;
use Drupal\Core\Routing\CacheableSecuredRedirectResponse;
use Drupal\Core\Routing\RequestContext;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Http\TrustedHostsRequestFactory;
use Drupal\Core\Site\Settings;
/**
* Provides a redirect response which understands domain URLs are local to the install.
*
* This class can be used in cases where LocalRedirectResponse needs to be domain
* sensitive. The main implementation is in DomainSourceRedirectResponseSubscriber.
*
* This class combines LocalAwareRedirectResponseTrait and UrlHelper methods that
* cannot be overridden safely otherwise.
*/
class DomainRedirectResponse extends CacheableSecuredRedirectResponse {
/**
* The request context.
*
* @var \Drupal\Core\Routing\RequestContext
*/
protected $requestContext;
/**
* The trusted host patterns.
*
* @var array
*/
protected static $trustedHostPatterns;
/**
* The trusted hosts matched by the settings.
*
* @var array
*/
protected static $trustedHosts;
/**
* {@inheritdoc}
*/
protected function isLocal($url) {
$base_url = $this->getRequestContext()->getCompleteBaseUrl();
return !UrlHelper::isExternal($url) || UrlHelper::externalIsLocal($url, $base_url) || $this->externalIsRegistered($url, $base_url);
}
/**
* {@inheritdoc}
*/
protected function isSafe($url) {
return $this->isLocal($url);
}
/**
* Returns the request context.
*
* @return \Drupal\Core\Routing\RequestContext
*/
protected function getRequestContext() {
if (!isset($this->requestContext)) {
$this->requestContext = \Drupal::service('router.request_context');
}
return $this->requestContext;
}
/**
* Sets the request context.
*
* @param \Drupal\Core\Routing\RequestContext $request_context
* The request context.
*
* @return $this
*/
public function setRequestContext(RequestContext $request_context) {
$this->requestContext = $request_context;
return $this;
}
/**
* Determines if an external URL points to this domain-aware installation.
*
* This method replaces the logic in
* Drupal\Component\Utility\UrlHelper::externalIsLocal(). Since that class is not
* directly extendable, we have to replace it.
*
* @param string $url
* A string containing an external URL, such as "http://example.com/foo".
*
* @return bool
* TRUE if the URL has the same domain and base path.
* @param string $base_url
* The base URL string to check against, such as "http://example.com/"
*
* @throws \InvalidArgumentException
* Exception thrown when $url is not fully qualified.
*/
public static function externalIsRegistered($url, $base_url) {
$url_parts = parse_url($url);
$base_parts = parse_url($base_url);
if (empty($url_parts['host'])) {
throw new \InvalidArgumentException('A path was passed when a fully qualified domain was expected.');
}
// Check that the host name is registered with trusted hosts.
$trusted = self::checkTrustedHost($url_parts['host']);
if (!$trusted) {
return FALSE;
}
// Check that the requested $url is registered.
$negotiator = \Drupal::service('domain.negotiator');
$registered_domain = $negotiator->isRegisteredDomain($url_parts['host']);
if (!isset($url_parts['path']) || !isset($base_parts['path'])) {
return $registered_domain;
}
else {
// When comparing base paths, we need a trailing slash to make sure a
// partial URL match isn't occurring. Since base_path() always returns
// with a trailing slash, we don't need to add the trailing slash here.
return ($registered_domain && stripos($url_parts['path'], $base_parts['path']) === 0);
}
}
/**
* Checks that a host is registered with trusted_host_patterns.
*
* This method is cribbed from Symfony's Request::getHost() method.
*
* @param string $host
* The hostname to check.
*
* @return bool
* TRUE if the hostname matches the trusted_host_patterns. FALSE otherwise.
* It is the caller's responsibility to deal with this result securely.
*/
public static function checkTrustedHost($host) {
// See Request::setTrustedHosts();
if (!isset(self::$trustedHostPatterns)) {
self::$trustedHostPatterns = array_map(function ($hostPattern) {
return sprintf('#%s#i', $hostPattern);
}, Settings::get('trusted_host_patterns', []));
// Reset the trusted host match array.
self::$trustedHosts = [];
}
// Trim and remove port number from host. Host is lowercase as per RFC 952/2181
$host = strtolower(preg_replace('/:\d+$/', '', trim($host)));
// In the original Symfony code, hostname validation runs here. We have removed that
// portion because Domains are already validated on creation.
if (count(self::$trustedHostPatterns) > 0) {
// To avoid host header injection attacks, you should provide a list of trusted host patterns
if (in_array($host, self::$trustedHosts)) {
return TRUE;
}
foreach (self::$trustedHostPatterns as $pattern) {
if (preg_match($pattern, $host)) {
self::$trustedHosts[] = $host;
return TRUE;
}
}
return FALSE;
}
// In cases where trusted_host_patterns are not set, allow all. This is flagged as a
// security issue by Drupal core in the Reports UI.
return TRUE;
}
}
@@ -0,0 +1,205 @@
<?php
namespace Drupal\domain;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\domain\DomainStorageInterface;
/**
* Loads Domain records.
*/
class DomainStorage extends ConfigEntityStorage implements DomainStorageInterface {
/**
* The typed config handler.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfig;
/**
* Constructs a DomainStorage object.
*
* Trying to inject the storage manager throws an exception.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed config handler.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, TypedConfigManagerInterface $typed_config) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->configFactory = $config_factory;
$this->uuidService = $uuid_service;
$this->languageManager = $language_manager;
$this->typedConfig = $typed_config;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('config.typed')
);
}
/**
* {@inheritdoc}
*/
public function loadSchema() {
$fields = $this->typedConfig->getDefinition('domain.record.*');
return isset($fields['mapping']) ? $fields['mapping'] : array();
}
/**
* {@inheritdoc}
*/
public function loadDefaultId() {
$result = $this->loadDefaultDomain();
if (!empty($result)) {
return $result->id();
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadDefaultDomain() {
$result = $this->loadByProperties(array('is_default' => TRUE));
if (!empty($result)) {
return current($result);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function loadMultipleSorted(array $ids = NULL) {
$domains = $this->loadMultiple($ids);
uasort($domains, array($this, 'sort'));
return $domains;
}
/**
* {@inheritdoc}
*/
public function loadByHostname($hostname) {
$hostname = $this->prepareHostname($hostname);
$result = $this->loadByProperties(array('hostname' => $hostname));
if (empty($result)) {
return NULL;
}
return current($result);
}
/**
* {@inheritdoc}
*/
public function loadOptionsList() {
$list = array();
foreach ($this->loadMultipleSorted() as $id => $domain) {
$list[$id] = $domain->label();
}
return $list;
}
/**
* {@inheritdoc}
*/
public function sort(DomainInterface $a, DomainInterface $b) {
return $a->getWeight() > $b->getWeight();
}
/**
* {@inheritdoc}
*/
public function prepareHostname($hostname) {
// Strip www. prefix off the hostname.
$ignore_www = $this->configFactory->get('domain.settings')->get('www_prefix');
if ($ignore_www && substr($hostname, 0, 4) == 'www.') {
$hostname = substr($hostname, 4);
}
return $hostname;
}
/**
* {@inheritdoc}
*/
public function create(array $values = []) {
$default = $this->loadDefaultId();
$domains = $this->loadMultiple();
if (empty($values)) {
$values['hostname'] = $this->createHostname();
$values['name'] = \Drupal::config('system.site')->get('name');
}
$values += array(
'scheme' => $this->getDefaultScheme(),
'status' => 1,
'weight' => count($domains) + 1,
'is_default' => (int) empty($default),
);
$domain = parent::create($values);
return $domain;
}
/**
* {@inheritdoc}
*/
public function createHostname() {
// We cannot inject the negotiator due to dependencies.
return \Drupal::service('domain.negotiator')->negotiateActiveHostname();
}
/**
* {@inheritdoc}
*/
public function createMachineName($hostname = NULL) {
if (empty($hostname)) {
$hostname = $this->createHostname();
}
return preg_replace('/[^a-z0-9_]/', '_', $hostname);
}
/**
* {@inheritdoc}
*/
public function getDefaultScheme() {
// Use the foundation request if possible/
$request = \Drupal::request();
if (!empty($request)) {
$scheme = $request->getScheme();
}
// Else use the server variable.
elseif (!empty($_SERVER['https'])) {
$scheme = 'https';
}
// Else fall through to default.
else {
$scheme = 'http';
}
return $scheme;
}
}
@@ -0,0 +1,124 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
/**
* Provides an interface for domain entity storage.
*/
interface DomainStorageInterface extends ConfigEntityStorageInterface {
/**
* Gets the default domain object.
*
* @return \Drupal\domain\DomainInterface|null
* The default domain record or NULL.
*/
public function loadDefaultDomain();
/**
* Returns the id of the default domain.
*
* @return int
* The id of the default domain or FALSE if none is set.
*/
public function loadDefaultId();
/**
* Loads multiple domains and sorts by weight.
*
* @param array $ids
* An optional array of specific ids to load.
*
* @return \Drupal\domain\DomainInterface[]
* An array of domain records.
*/
public function loadMultipleSorted(array $ids = NULL);
/**
* Loads a domain record by hostname lookup.
*
* @param string $hostname
* A hostname string, in the format example.com.
*
* @return \Drupal\domain\DomainInterface|null
* The domain record or NULL.
*/
public function loadByHostname($hostname);
/**
* Returns the list of domains formatted for a form options list.
*
* @return array
* A weight-sorted array of id => label for use in forms.
*/
public function loadOptionsList();
/**
* Sorts domains by weight.
*
* For use by loadMultipleSorted().
*
* @param DomainInterface $a
* The first Domain object to sort.
* @param DomainInterface $b
* The Domain object to compare against.
*
* @return bool
* Wether the first domain weight is greater or not.
*/
public function sort(DomainInterface $a, DomainInterface $b);
/**
* Gets the entity field schema for domain records.
*
* @return array
* An array representing the field schema of the object.
*/
public function loadSchema();
/**
* Removes www. prefix from a hostname, if set.
*
* @param string $hostname
* A hostname.
*
* @return string
* The cleaned hostname.
*/
public function prepareHostname($hostname);
/**
* Gets the hostname of the active request.
*
* @return string
* The hostname string of the current request.
*/
public function createHostname();
/**
* Creates a machine-name string from the hostname.
*
* This string is the primary key of the entity.
*
* @param string $hostname
* The hostname of the domain record. If empty, the current request will be
* used.
*
* @return string
* A string containing A-Z, a-z, 0-9, and _ characters.
*/
public function createMachineName($hostname = NULL);
/**
* Returns the default http/https scheme for the site.
*
* This function helps us account for variable schemes across environments.
*
* @return $scheme
* A string representation of s scheme (http|https).
*/
public function getDefaultScheme();
}
@@ -0,0 +1,187 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Handles requests for token creation.
*
* TokenAPI still uses procedural code, but we have moved it to a class for
* easier refactoring.
*/
/**
* Token handler for Domain.
*/
class DomainToken {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The Domain storage handler.
*
* @var \Drupal\domain\DomainStorageInterface $domainStorage
*/
protected $domainStorage;
/**
* The Domain negotiator.
*
* @var \Drupal\domain\DomainNegotiatorInterface $negotiator
*/
protected $negotiator;
/**
* Constructs a DomainToken object.
*
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\domain\DomainNegotiatorInterface $negotiator
* The domain negotiator.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, DomainNegotiatorInterface $negotiator) {
$this->entityTypeManager = $entity_type_manager;
$this->domainStorage = $this->entityTypeManager->getStorage('domain');
$this->negotiator = $negotiator;
}
/**
* Implements hook_token_info().
*/
public function getTokenInfo() {
// Domain token types.
$info['types']['domain'] = array(
'name' => $this->t('Domains'),
'description' => $this->t('Tokens related to domains.'),
'needs-data' => 'domain',
);
// These two types require the Token contrib module.
$info['types']['current-domain'] = array(
'name' => $this->t('Current domain'),
'description' => $this->t('Tokens related to the current domain.'),
'type' => 'domain',
);
$info['types']['default-domain'] = array(
'name' => $this->t('Default domain'),
'description' => $this->t('Tokens related to the default domain.'),
'type' => 'domain',
);
// Domain tokens.
$info['tokens']['domain']['id'] = array(
'name' => $this->t('Domain id'),
'description' => $this->t('The domain\'s numeric ID.'),
);
$info['tokens']['domain']['machine-name'] = array(
'name' => $this->t('Domain machine name'),
'description' => $this->t('The domain machine identifier.'),
);
$info['tokens']['domain']['path'] = array(
'name' => $this->t('Domain path'),
'description' => $this->t('The base URL for the domain.'),
);
$info['tokens']['domain']['name'] = array(
'name' => $this->t('Domain name'),
'description' => $this->t('The domain name.'),
);
$info['tokens']['domain']['url'] = array(
'name' => $this->t('Domain URL'),
'description' => $this->t('The domain\'s URL for the current page request.'),
);
$info['tokens']['domain']['hostname'] = array(
'name' => $this->t('Domain hostname'),
'description' => $this->t('The domain hostname.'),
);
$info['tokens']['domain']['scheme'] = array(
'name' => $this->t('Domain scheme'),
'description' => $this->t('The domain scheme.'),
);
$info['tokens']['domain']['status'] = array(
'name' => $this->t('Domain status'),
'description' => $this->t('The domain status.'),
);
$info['tokens']['domain']['weight'] = array(
'name' => $this->t('Domain weight'),
'description' => $this->t('The domain weight.'),
);
$info['tokens']['domain']['is_default'] = array(
'name' => $this->t('Domain default'),
'description' => $this->t('The domain is the default domain.'),
);
return $info;
}
/**
* Implements hook_tokens().
*/
public function getTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$replacements = array();
$domain = NULL;
// Based on the type, get the proper domain context.
switch ($type) {
case 'domain':
if (!empty($data['domain'])) {
$domain = $data['domain'];
}
else {
$domain = $this->negotiator->getActiveDomain();
}
break;
case 'current-domain':
$domain = $this->negotiator->getActiveDomain();
break;
case 'default-domain':
$domain = $this->domainStorage->loadDefaultDomain();
break;
}
// Set the token information.
if (!empty($domain)) {
$callbacks = $this->getCallbacks();
foreach ($tokens as $name => $original) {
if (isset($callbacks[$name])) {
$replacements[$original] = $domain->{$callbacks[$name]}();
$bubbleable_metadata->addCacheableDependency($domain);
}
}
}
return $replacements;
}
/**
* Maps tokens to their entity callbacks.
*
* We assume that the token will call an instance of DomainInterface.
*
* @return array
*/
public function getCallbacks() {
return [
'id' => 'getDomainId',
'machine-name' => 'id',
'path' => 'getPath',
'name' => 'label',
'hostname' => 'getHostname',
'scheme' => 'getScheme',
'status' => 'status',
'weight' => 'getWeight',
'is_default' => 'isDefault',
'url' => 'getUrl',
];
}
}
@@ -0,0 +1,154 @@
<?php
namespace Drupal\domain;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Component\Utility\Unicode;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Provides validation of domain strings against RFC standards for hostnames.
*/
class DomainValidator implements DomainValidatorInterface {
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a DomainValidator object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \GuzzleHttp\ClientInterface $http_client
* A Guzzle client object.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, ClientInterface $http_client, EntityTypeManagerInterface $entity_type_manager) {
$this->moduleHandler = $module_handler;
$this->configFactory = $config_factory;
$this->httpClient = $http_client;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function validate($hostname) {
$error_list = array();
// Check for at least one dot or the use of 'localhost'.
// Note that localhost can specify a port.
$localhost_check = explode(':', $hostname);
if (substr_count($hostname, '.') == 0 && $localhost_check[0] != 'localhost') {
$error_list[] = $this->t('At least one dot (.) is required, except when using <em>localhost</em>.');
}
// Check for one colon only.
if (substr_count($hostname, ':') > 1) {
$error_list[] = $this->t('Only one colon (:) is allowed.');
}
// If a colon, make sure it is only followed by numbers.
elseif (substr_count($hostname, ':') == 1) {
$parts = explode(':', $hostname);
$port = (int) $parts[1];
if (strcmp($port, $parts[1])) {
$error_list[] = $this->t('The port protocol must be an integer.');
}
}
// The domain cannot begin or end with a period.
if (substr($hostname, 0, 1) == '.') {
$error_list[] = $this->t('The domain must not begin with a dot (.)');
}
// The domain cannot begin or end with a period.
if (substr($hostname, -1) == '.') {
$error_list[] = $this->t('The domain must not end with a dot (.)');
}
// Check for valid characters, unless using non-ASCII domains.
$config = $this->configFactory->get('domain.settings');
$non_ascii = $config->get('allow_non_ascii');
if (!$non_ascii) {
$pattern = '/^[a-z0-9\.\-:]*$/i';
if (!preg_match($pattern, $hostname)) {
$error_list[] = $this->t('Only alphanumeric characters, dashes, and a colon are allowed.');
}
}
// Check for lower case.
if ($hostname != Unicode::strtolower($hostname)) {
$error_list[] = $this->t('Only lower-case characters are allowed.');
}
// Check for 'www' prefix if redirection / handling is
// enabled under global domain settings.
// Note that www prefix handling must be set explicitly in the UI.
// See http://drupal.org/node/1529316 and http://drupal.org/node/1783042
if ($config->get('www_prefix') && (substr($hostname, 0, strpos($hostname, '.')) == 'www')) {
$error_list[] = $this->t('WWW prefix handling: Domains must be registered without the www. prefix.');
}
// Allow modules to alter this behavior.
$this->moduleHandler->alter('domain_validate', $error_list, $hostname);
return $error_list;
}
/**
* {@inheritdoc}
*/
public function checkResponse(DomainInterface $domain) {
$url = $domain->getPath() . drupal_get_path('module', 'domain') . '/tests/200.png';
try {
// GuzzleHttp no longer allows for bogus URL calls.
$request = $this->httpClient->get($url);
}
// We cannot know which Guzzle Exception class will be returned; be generic.
catch (RequestException $e) {
watchdog_exception('domain', $e);
// File a general server failure.
$domain->setResponse(500);
return;
}
// Expected result (i.e. no exception thrown.)
$domain->setResponse($request->getStatusCode());
return $domain->getResponse();
}
/**
* {@inheritdoc}
*/
public function getRequiredFields() {
return array('hostname', 'name', 'scheme', 'status', 'weight');
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\domain;
/**
* Supplies validator methods for common domain requests.
*/
interface DomainValidatorInterface {
/**
* Validates the hostname for a domain.
*
* @param string $hostname
* A hostname to validate.
*
* @return array
* An array of validation errors. An empty array indicates a valid domain.
*/
public function validate($hostname);
/**
* Tests that a domain responds correctly.
*
* This is a server-level configuration test. The core module provides an
* image file that we use to test the validity of domain-generated URLs.
*
* That file is /domain/tests/200.png.
*
* @param \Drupal\domain\DomainInterface $domain
* A domain record.
*
* @return int
* The server response code for the request.
*/
public function checkResponse(DomainInterface $domain);
/**
* Returns the properties required to create a domain record.
*
* @return array
* Array of property names.
*/
public function getRequiredFields();
}
@@ -0,0 +1,565 @@
<?php
namespace Drupal\domain\Entity;
use Drupal\Core\Config\ConfigValueException;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\domain\DomainInterface;
use Drupal\domain\DomainNegotiator;
/**
* Defines the domain entity.
*
* @ConfigEntityType(
* id = "domain",
* label = @Translation("Domain record"),
* module = "domain",
* handlers = {
* "storage" = "Drupal\domain\DomainStorage",
* "access" = "Drupal\domain\DomainAccessControlHandler",
* "list_builder" = "Drupal\domain\DomainListBuilder",
* "form" = {
* "default" = "Drupal\domain\DomainForm",
* "edit" = "Drupal\domain\DomainForm",
* "delete" = "Drupal\domain\Form\DomainDeleteForm"
* }
* },
* config_prefix = "record",
* admin_permission = "administer domains",
* entity_keys = {
* "id" = "id",
* "domain_id" = "domain_id",
* "label" = "name",
* "uuid" = "uuid",
* "status" = "status",
* "weight" = "weight"
* },
* links = {
* "delete-form" = "/admin/config/domain/delete/{domain}",
* "edit-form" = "/admin/config/domain/edit/{domain}",
* "collection" = "/admin/config/domain",
* },
* uri_callback = "domain_uri",
* config_export = {
* "id",
* "domain_id",
* "hostname",
* "name",
* "scheme",
* "status",
* "weight",
* "is_default",
* }
* )
*/
class Domain extends ConfigEntityBase implements DomainInterface {
use StringTranslationTrait;
/**
* The ID of the domain entity.
*
* @var string
*/
protected $id;
/**
* The domain record ID.
*
* @var int
*/
protected $domain_id;
/**
* The domain list name (e.g. Drupal).
*
* @var string
*/
protected $name;
/**
* The domain hostname (e.g. example.com).
*
* @var string
*/
protected $hostname;
/**
* The domain record sort order.
*
* @var int
*/
protected $weight;
/**
* Indicates the default domain.
*
* @var bool
*/
protected $is_default;
/**
* The domain record protocol (e.g. http://).
*
* @var string
*/
protected $scheme;
/**
* The domain record base path, a calculated value.
*
* @var string
*/
protected $path;
/**
* The domain record current url, a calculated value.
*
* @var string
*/
protected $url;
/**
* The domain record http response test (e.g. 200), a calculated value.
*
* @var int
*/
protected $response = NULL;
/**
* The redirect method to use, if needed.
*
* @var int|null
*/
protected $redirect = NULL;
/**
* The type of match returned by the negotiator.
*
* @var int
*/
protected $matchType;
/**
* The canonical hostname for the domain.
*/
protected $canonical;
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
parent::preCreate($storage_controller, $values);
$domain_storage = \Drupal::service('entity_type.manager')->getStorage('domain');
$default = $domain_storage->loadDefaultId();
$count = $storage_controller->getQuery()->count()->execute();
$values += array(
'scheme' => empty($GLOBALS['is_https']) ? 'http' : 'https',
'status' => 1,
'weight' => $count + 1,
'is_default' => (int) empty($default),
);
// Note that we have not created a domain_id, which is only used for
// node access control and will be added on save.
}
/**
* {@inheritdoc}
*/
public function isActive() {
$negotiator = \Drupal::service('domain.negotiator');
/** @var self $domain */
$domain = $negotiator->getActiveDomain();
if (empty($domain)) {
return FALSE;
}
return ($this->id() == $domain->id());
}
/**
* {@inheritdoc}
*/
public function addProperty($name, $value) {
if (!isset($this->{$name})) {
$this->{$name} = $value;
}
}
/**
* {@inheritdoc}
*/
public function isDefault() {
return (bool) $this->is_default;
}
/**
* {@inheritdoc}
*/
public function isHttps() {
return (bool) ($this->getScheme(FALSE) == 'https');
}
/**
* {@inheritdoc}
*/
public function saveDefault() {
if (!$this->isDefault()) {
// Swap the current default.
/** @var self $default */
if ($default = \Drupal::service('entity_type.manager')->getStorage('domain')->loadDefaultDomain()) {
$default->is_default = 0;
$default->setHostname($default->getCanonical());
$default->save();
}
// Save the new default.
$this->is_default = 1;
$this->setHostname($this->getCanonical());
$this->save();
}
else {
drupal_set_message($this->t('The selected domain is already the default.'), 'warning');
}
}
/**
* {@inheritdoc}
*/
public function enable() {
$this->setStatus(TRUE);
$this->setHostname($this->getCanonical());
$this->save();
}
/**
* {@inheritdoc}
*/
public function disable() {
if (!$this->isDefault()) {
$this->setStatus(FALSE);
$this->setHostname($this->getCanonical());
$this->save();
}
else {
drupal_set_message($this->t('The default domain cannot be disabled.'), 'warning');
}
}
/**
* {@inheritdoc}
*/
public function saveProperty($name, $value) {
if (isset($this->{$name})) {
$this->{$name} = $value;
$this->setHostname($this->getCanonical());
$this->save();
drupal_set_message($this->t('The @key attribute was set to @value for domain @hostname.', array(
'@key' => $name,
'@value' => $value,
'@hostname' => $this->hostname,
)));
}
else {
drupal_set_message($this->t('The @key attribute does not exist.', array('@key' => $name)));
}
}
/**
* {@inheritdoc}
*/
public function setPath() {
$this->path = $this->getScheme() . $this->getHostname() . base_path();
}
/**
* {@inheritdoc}
*/
public function setUrl() {
$uri = \Drupal::request()->getRequestUri();
$this->url = $this->getScheme() . $this->getHostname() . $uri;
}
/**
* {@inheritdoc}
*/
public function getPath() {
if (!isset($this->path)) {
$this->setPath();
}
return $this->path;
}
/**
* Returns the raw path of the domain object, without the base url.
*/
public function getRawPath() {
return $this->getScheme() . $this->getHostname();
}
/**
* Builds a link from a known internal path.
*
* @param string $path
* A Drupal-formatted internal path, starting with /. Note that it is the
* caller's responsibility to handle the base_path().
*
* @return string
* The built link.
*/
public function buildUrl($path) {
return $this->getRawPath() . $path;
}
/**
* {@inheritdoc}
*/
public function getUrl() {
if (!isset($this->url)) {
$this->setUrl();
}
return $this->url;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Sets the default domain properly.
/** @var self $default */
$default = $storage->loadDefaultDomain();
if (!$default) {
$this->is_default = 1;
}
elseif ($this->is_default && $default->getDomainId() != $this->getDomainId()) {
// Swap the current default.
$default->is_default = 0;
$default->save();
}
// Ensures we have a proper domain_id.
if ($this->isNew()) {
$this->createDomainId();
}
// Prevent duplicate hostname.
$hostname = $this->getHostname();
// Do not use domain loader because it may change hostname.
$existing = $storage->loadByProperties(['hostname' => $hostname]);
$existing = reset($existing);
if ($existing && $this->getDomainId() != $existing->getDomainId()) {
throw new ConfigValueException("The hostname ($hostname) is already registered.");
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Invalidate cache tags relevant to domains.
\Drupal::service('cache_tags.invalidator')->invalidateTags(['rendered', 'url.site']);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
foreach ($entities as $entity) {
$actions = $storage->loadMultiple([
'domain_default_action.' . $entity->id(),
'domain_delete_action.' . $entity->id(),
'domain_disable_action.' . $entity->id(),
'domain_enable_action.' . $entity->id(),
]);
foreach ($actions as $action) {
$action->delete();
}
}
// Invalidate cache tags relevant to domains.
\Drupal::service('cache_tags.invalidator')->invalidateTags(['rendered', 'url.site']);
}
/**
* {@inheritdoc}
*/
public function createDomainId() {
// We cannot reliably use sequences (1, 2, 3) because those can be different
// across environments. Instead, we use the crc32 hash function to create a
// unique numeric id for each domain. In some systems (Windows?) we have
// reports of crc32 returning a negative number. Issue #2794047.
// If we don't use hash(), then crc32() returns different results for 32- and 64-bit
// systems. On 32-bit systems, the number returned may also be too large for PHP.
// See #2908236.
$id = hash('crc32', $this->id());
$id = abs(hexdec(substr($id, 0, -2)));
$this->createNumericId($id);
}
/**
* Creates a unique numeric id for use in the {node_access} table.
*
* @param integer $id
* An integer to ue as the numeric id.
*/
public function createNumericId($id) {
// Ensure that this value is unique.
$storage = \Drupal::entityTypeManager()->getStorage('domain');
$result = $storage->loadByProperties(array('domain_id' => $id));
if (empty($result)) {
$this->domain_id = $id;
}
else {
$id++;
$this->createNumericId($id);
}
}
/**
* {@inheritdoc}
*/
public function getScheme($add_suffix = TRUE) {
$scheme = $this->scheme;
if ($scheme == 'variable') {
$scheme = \Drupal::service('entity_type.manager')->getStorage('domain')->getDefaultScheme();
}
elseif ($scheme != 'https') {
$scheme = 'http';
}
$scheme .= ($add_suffix) ? '://' : '';
return $scheme;
}
/**
* {@inheritdoc}
*/
public function getRawScheme() {
return $this->scheme;
}
/**
* {@inheritdoc}
*/
public function getResponse() {
if (empty($this->response)) {
$validator = \Drupal::service('domain.validator');
$validator->checkResponse($this);
}
return $this->response;
}
/**
* {@inheritdoc}
*/
public function setResponse($response) {
$this->response = $response;
}
/**
* {@inheritdoc}
*/
public function getLink($current_path = TRUE) {
$options = array('absolute' => TRUE, 'https' => $this->isHttps());
if ($current_path) {
$url = Url::fromUri($this->getUrl(), $options);
}
else {
$url = Url::fromUri($this->getPath(), $options);
}
return Link::fromTextAndUrl($this->getCanonical(), $url)->toString();
}
/**
* {@inheritdoc}
*/
public function getRedirect() {
return $this->redirect;
}
/**
* {@inheritdoc}
*/
public function setRedirect($code = 302) {
$this->redirect = $code;
}
/**
* {@inheritdoc}
*/
public function getHostname() {
return $this->hostname;
}
/**
* {@inheritdoc}
*/
public function setHostname($hostname) {
$this->hostname = $hostname;
}
/**
* {@inheritdoc}
*/
public function getDomainId() {
return $this->domain_id;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function setMatchType($match_type = DomainNegotiator::DOMAIN_MATCH_EXACT) {
$this->matchType = $match_type;
}
/**
* {@inheritdoc}
*/
public function getMatchType() {
return $this->matchType;
}
/**
* {@inheritdoc}
*/
public function getPort() {
$ports = explode(':', $this->getHostname());
if (isset($ports[1])) {
return ':' . $ports[1];
}
return '';
}
/**
* {@inheritdoc}
*/
public function setCanonical($hostname = NULL) {
if (is_null($hostname)) {
$this->canonical = $this->getHostname();
}
else {
$this->canonical = $hostname;
}
}
/**
* {@inheritdoc}
*/
public function getCanonical() {
if (empty($this->canonical)) {
$this->setCanonical();
}
return $this->canonical;
}
}
@@ -0,0 +1,140 @@
<?php
namespace Drupal\domain\EventSubscriber;
use Drupal\domain\Access\DomainAccessCheck;
use Drupal\domain\DomainNegotiatorInterface;
use Drupal\domain\DomainStorageInterface;
use Drupal\domain\DomainRedirectResponse;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Response;
/**
* Sets the domain context for an http request.
*/
class DomainSubscriber implements EventSubscriberInterface {
/**
* The domain negotiator service.
*
* @var \Drupal\domain\DomainNegotiatorInterface
*/
protected $domainNegotiator;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The Domain storage handler service.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* The core access check service.
*
* @var \Drupal\Core\Access\AccessCheckInterface
*/
protected $accessCheck;
/**
* The current user account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a DomainSubscriber object.
*
* @param \Drupal\domain\DomainNegotiatorInterface $negotiator
* The domain negotiator service.
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\domain\Access\DomainAccessCheck $access_check
* The access check interface.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user account.
*/
public function __construct(DomainNegotiatorInterface $negotiator, EntityTypeManagerInterface $entity_type_manager, DomainAccessCheck $access_check, AccountInterface $account) {
$this->domainNegotiator = $negotiator;
$this->entityTypeManager = $entity_type_manager;
$this->domainStorage = $this->entityTypeManager->getStorage('domain');
$this->accessCheck = $access_check;
$this->account = $account;
}
/**
* Sets the domain context of the request.
*
* This method also determines the redirect status for the http request.
*
* Specifically, here we determine if a redirect is required. That happens
* in one of two cases: an unauthorized request to an inactive domain is made;
* a domain alias is set to redirect to its primary domain record.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*
* @see domain_alias_domain_request_alter
*/
public function onKernelRequestDomain(GetResponseEvent $event) {
// Negotiate the request and set domain context.
/** @var \Drupal\domain\DomainInterface $domain */
if ($domain = $this->domainNegotiator->getActiveDomain(TRUE)) {
$hostname = $domain->getHostname();
$domain_url = $domain->getUrl();
if ($domain_url) {
$redirect_status = $domain->getRedirect();
$path = trim($event->getRequest()->getPathInfo(), '/');
// If domain negotiation asked for a redirect, issue it.
if (is_null($redirect_status) && $this->accessCheck->checkPath($path)) {
// Else check for active domain or inactive access.
/** @var \Drupal\Core\Access\AccessResult $access */
$access = $this->accessCheck->access($this->account);
// If the access check fails, reroute to the default domain.
// Note that Allowed, Neutral, and Failed are the options here.
// We insist on Allowed.
if (!$access->isAllowed()) {
/** @var \Drupal\domain\DomainInterface $default */
$default = $this->domainStorage->loadDefaultDomain();
$domain_url = $default->getUrl();
$redirect_status = 302;
$hostname = $default->getHostname();
}
}
}
if (!empty($redirect_status)) {
// Pass a redirect if necessary.
if (DomainRedirectResponse::checkTrustedHost($hostname)) {
$response = new TrustedRedirectResponse($domain_url, $redirect_status);
}
else {
// If the redirect is not to a registered hostname, reject the request.
$response = new Response('The provided host name is not a valid redirect.', 401);
}
$event->setResponse($response);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// This needs to fire very early in the stack, before accounts are cached.
$events[KernelEvents::REQUEST][] = array('onKernelRequestDomain', 50);
return $events;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\domain\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Builds the form to delete a domain record.
*/
class DomainDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('domain.admin');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
drupal_set_message($this->t('Domain %label has been deleted.', array('%label' => $this->entity->label())));
\Drupal::logger('domain')->notice('Domain %label has been deleted.', array('%label' => $this->entity->label()));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @file
* Settings form for Domain module.
*/
namespace Drupal\domain\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
class DomainSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'domain_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['domain.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('domain.settings');
$form['allow_non_ascii'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Allow non-ASCII characters in domains and aliases'),
'#default_value' => $config->get('allow_non_ascii'),
'#description' => $this->t('Domains may be registered with international character sets. Note that not all DNS server respect non-ascii characters.'),
);
$form['www_prefix'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Ignore www prefix when negotiating domains'),
'#default_value' => $config->get('www_prefix'),
'#description' => $this->t('Domain negotiation will ignore any www prefixes for all requests.'),
);
// Get the usable tokens for this field.
foreach (\Drupal::service('domain.token')->getCallbacks() as $key => $callback) {
$patterns[] = "[domain:$key]";
}
$form['css_classes'] = array(
'#type' => 'textfield',
'#size' => 80,
'#title' => $this->t('Custom CSS classes'),
'#default_value' => $config->get('css_classes'),
'#description' => $this->t('Enter any CSS classes that should be added to the &lt;body&gt; tag. Available replacement patterns are: ' . implode(', ', $patterns)),
);
$form['login_paths'] = array(
'#type' => 'textarea',
'#rows' => 5,
'#columns' => 40,
'#title' => $this->t('Paths that should be accessible for inactive domains'),
'#default_value' => $config->get('login_paths', "/user/login\r\n/user/password"),
'#description' => $this->t('Inactive domains are only accessible to users with permission.
Enter any paths that should be accessible, one per line. Normally, only the
login path will be allowed.'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('domain.settings');
foreach ($this->settingsKeys() as $key) {
$config->set($key, $form_state->getValue($key));
}
$config->save();
parent::submitForm($form, $form_state);
}
/**
* Returns an array of settings keys.
*/
public function settingsKeys() {
return [
'allow_non_ascii',
'www_prefix',
'login_paths',
'css_classes',
];
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\domain\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Sets the domain is_default property to 1.
*
* @Action(
* id = "domain_default_action",
* label = @Translation("Set default domain record"),
* type = "domain"
* )
*/
class DefaultDomain extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute(DomainInterface $domain = NULL) {
$domain->saveDefault();
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $objects) {
foreach ($objects as $object) {
if ($object instanceof DomainInterface) {
$object->saveDefault();
}
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access_result = AccessResult::allowedIfHasPermission($account, 'administer domains');
return $return_as_object ? $access_result : $access_result->isAllowed();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\domain\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Deletes a domain record.
*
* @Action(
* id = "domain_delete_action",
* label = @Translation("Delete domain record"),
* type = "domain"
* )
*/
class DeleteDomain extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute(DomainInterface $domain = NULL) {
$domain->delete();
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $objects) {
foreach ($objects as $object) {
if ($object instanceof DomainInterface) {
$object->delete();
}
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access_result = AccessResult::allowedIfHasPermission($account, 'administer domains');
return $return_as_object ? $access_result : $access_result->isAllowed();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\domain\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Sets the domain status property to 0.
*
* @Action(
* id = "domain_disable_action",
* label = @Translation("Disable domain record"),
* type = "domain"
* )
*/
class DisableDomain extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute(DomainInterface $domain = NULL) {
$domain->disable();
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $objects) {
foreach ($objects as $object) {
if ($object instanceof DomainInterface) {
$object->disable();
}
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access_result = AccessResult::allowedIfHasPermission($account, 'administer domains');
return $return_as_object ? $access_result : $access_result->isAllowed();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Drupal\domain\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Sets the domain status property to 1.
*
* @Action(
* id = "domain_enable_action",
* label = @Translation("Enable domain record"),
* type = "domain"
* )
*/
class EnableDomain extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute(DomainInterface $domain = NULL) {
$domain->enable();
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $objects) {
foreach ($objects as $object) {
if ($object instanceof DomainInterface) {
$object->enable();
}
}
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access_result = AccessResult::allowedIfHasPermission($account, 'administer domains');
return $return_as_object ? $access_result : $access_result->isAllowed();
}
}
@@ -0,0 +1,20 @@
<?php
namespace Drupal\domain\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Creates a common block pattern for caching.
*/
abstract class DomainBlockBase extends BlockBase {
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// By default, all domain blocks are per-url.
return ['url'];
}
}
@@ -0,0 +1,213 @@
<?php
namespace Drupal\domain\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Provides a block that links to all active domains.
*
* @Block(
* id = "domain_nav_block",
* admin_label = @Translation("Domain navigation")
* )
*/
class DomainNavBlock extends DomainBlockBase {
/**
* An array of settings.
*/
public $settings = [];
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
// By default, all domain blocks are per-url. This block, though, can be
// cached by url.site if we are printing the homepage path, not the request.
if ($this->getSetting('link_options') == 'home') {
return ['url.site'];
}
return ['url'];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$defaults = $this->defaultConfiguration();
$elements['link_options'] = [
'#title' => $this->t('Link paths'),
'#type' => 'radios',
'#required' => TRUE,
'#options' => ['active' => $this->t('Link to active url'), 'home' => $this->t('Link to site home page')],
'#default_value' => !empty($this->configuration['link_options']) ? $this->configuration['link_options'] : $defaults['link_options'],
'#description' => $this->t('Determines how links to each domain will be written. Note that some paths may not be accessible on all domains.'),
];
$options = array(
'select' => t('JavaScript select list'),
'menus' => t('Menu-style tab links'),
'ul' => t('Unordered list of links'),
);
$elements['link_theme'] = array(
'#type' => 'radios',
'#title' => t('Link theme'),
'#default_value' => !empty($this->configuration['link_theme']) ? $this->configuration['link_theme'] : $defaults['link_theme'],
'#options' => $options,
'#description' => $this->t('Select how to display the block output.'),
);
$options = array(
'name' => t('The domain display name'),
'hostname' => t('The raw hostname'),
'url' => t('The domain base URL'),
);
$elements['link_label'] = array(
'#type' => 'radios',
'#title' => t('Link text'),
'#default_value' => !empty($this->configuration['link_label']) ? $this->configuration['link_label'] : $defaults['link_label'],
'#options' => $options,
'#description' => $this->t('Select the text to display for each link.'),
);
return $elements;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
// Process the block's submission handling if no errors occurred only.
if (!$form_state->getErrors()) {
foreach (array_keys($this->defaultConfiguration()) as $element) {
$this->configuration[$element] = $form_state->getValue($element);
}
}
}
/**
* Overrides \Drupal\block\BlockBase::access().
*/
public function access(AccountInterface $account, $return_as_object = FALSE) {
$access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'use domain nav block'), 'OR');
return $return_as_object ? $access : $access->isAllowed();
}
/**
* Build the output.
*/
public function build() {
/** @var \Drupal\domain\DomainInterface $active_domain */
$active_domain = \Drupal::service('domain.negotiator')->getActiveDomain();
$access_handler = \Drupal::service('entity_type.manager')->getAccessControlHandler('domain');
$account = \Drupal::currentUser();
// Determine the visible domain list.
$items = [];
$add_path = ($this->getSetting('link_options') == 'active');
/** @var \Drupal\domain\DomainInterface $domain */
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultipleSorted() as $domain) {
// Set the URL.
$options = ['absolute' => TRUE, 'https' => ($domain->getScheme() == 'https')];
if ($add_path) {
$url = Url::fromUri($domain->getUrl(), $options);
}
else {
$url = Url::fromUri($domain->getPath(), $options);
}
// Set the label text.
$label = $domain->label();
if ($this->getSetting('link_label') == 'hostname') {
$label = $domain->getHostname();
}
elseif ($this->getSetting('link_label') == 'url') {
$label = $domain->getPath();
}
// Handles menu links.
if ($domain->status() || $account->hasPermission('access inactive domains')) {
if ($this->getSetting('link_theme') == 'menus') {
// @TODO: active trail isn't working properly, likely because this
// isn't really a menu.
$items[] = [
'title' => $label,
'url' => $url,
'attributes' => [],
'below' => [],
'is_expanded' => FALSE,
'is_collapsed' => FALSE,
'in_active_trail' => $domain->isActive(),
];
}
elseif ($this->getSetting('link_theme') == 'select') {
$items[] = [
'label' => $label,
'url' => $url->toString(),
'active' => $domain->isActive(),
];
}
else {
$items[] = ['#markup' => Link::fromTextAndUrl($label, $url)->toString()];
}
}
}
// Set the proper theme.
switch ($this->getSetting('link_theme')) {
case 'select':
$build['#theme'] = 'domain_nav_block';
$build['items'] = $items;
break;
case 'menus':
// Map the $items params to what menu.html.twig expects.
$build['#items'] = $items;
$build['#menu_name'] = 'domain-nav';
$build['#sorted'] = TRUE;
$build['#theme'] = 'menu__' . strtr($build['#menu_name'], '-', '_');
break;
case 'ul':
default:
$build['#theme'] = 'item_list';
$build['#items'] = $items;
break;
}
return $build;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'link_options' => 'home',
'link_theme' => 'ul',
'link_label' => 'name',
];
}
/**
* Gets the configuration for the block, loading defaults if not set.
*
* @param $key
* The setting key to retrieve, a string
*
* @return
*. The setting value, a string.
*/
public function getSetting($key) {
if (isset($this->settings[$key])) {
return $this->settings[$key];
}
$defaults = $this->defaultConfiguration();
if (isset($this->configuration[$key])) {
$this->settings[$key] = $this->configuration[$key];
}
else {
$this->settings[$key] = $defaults[$key];
}
return $this->settings[$key];
}
}
@@ -0,0 +1,126 @@
<?php
namespace Drupal\domain\Plugin\Block;
use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Provides a server information block for a domain request.
*
* @Block(
* id = "domain_server_block",
* admin_label = @Translation("Domain server information")
* )
*/
class DomainServerBlock extends DomainBlockBase {
/**
* Overrides \Drupal\block\BlockBase::access().
*/
public function access(AccountInterface $account, $return_as_object = FALSE) {
$access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'view domain information'), 'OR');
return $return_as_object ? $access : $access->isAllowed();
}
/**
* Build the output.
*
* @TODO: abstract or theme this function?
*/
public function build() {
/** @var \Drupal\domain\DomainInterface $domain */
$domain = \Drupal::service('domain.negotiator')->getActiveDomain();
if (!$domain) {
return array(
'#markup' => $this->t('No domain record could be loaded.'),
);
}
$header = array($this->t('Server'), $this->t('Value'));
$rows[] = array(
$this->t('HTTP_HOST request'),
Html::escape($_SERVER['HTTP_HOST']),
);
// Check the response test.
$domain->getResponse();
$check = \Drupal::service('entity_type.manager')->getStorage('domain')->loadByHostname($_SERVER['HTTP_HOST']);
$match = $this->t('Exact match');
if (!$check) {
// Specific check for Domain Alias.
if (isset($domain->alias)) {
$match = $this->t('ALIAS: Using alias %id', array('%id' => $domain->alias->getPattern()));
}
else {
$match = $this->t('FALSE: Using default domain.');
}
}
$rows[] = array(
$this->t('Domain match'),
$match,
);
$www = \Drupal::config('domain.settings')->get('www_prefix');
$rows[] = array(
$this->t('Strip www prefix'),
!empty($www) ? $this->t('On') : $this->t('Off'),
);
$list = $domain->toArray();
ksort($list);
foreach ($list as $key => $value) {
if (is_null($value)) {
$value = $this->t('NULL');
}
elseif ($value === TRUE) {
$value = $this->t('TRUE');
}
elseif ($value === FALSE) {
$value = $this->t('FALSE');
}
elseif ($key == 'status' || $key == 'is_default') {
$value = empty($value) ? $this->t('FALSE') : $this->t('TRUE');
}
$rows[] = array(
Html::escape($key),
!is_array($value) ? Html::escape($value) : $this->printArray($value),
);
}
return array(
'#theme' => 'table',
'#rows' => $rows,
'#header' => $header,
);
}
/**
* Prints array data for the server block.
*
* @param array $array
* An array of data. Note that we support two levels of nesting.
*
* @return string
* A suitable output string.
*/
public function printArray(array $array) {
$items = array();
foreach ($array as $key => $val) {
if (!is_array($val)) {
$value = Html::escape($val);
}
else {
$list = array();
foreach ($val as $k => $v) {
$list[] = $this->t('@key : @value', array('@key' => $k, '@value' => $v));
}
$value = implode('<br />', $list);
}
$items[] = $this->t('@key : @value', array('@key' => $key, '@value' => $value));
}
$variables['domain_server'] = array(
'#theme' => 'item_list',
'#items' => $items,
);
return render($variables);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\domain\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a block that links to all domains.
*
* @Block(
* id = "domain_switcher_block",
* admin_label = @Translation("Domain switcher (for admins and testing)")
* )
*/
class DomainSwitcherBlock extends DomainBlockBase {
/**
* Overrides \Drupal\block\BlockBase::access().
*/
public function access(AccountInterface $account, $return_as_object = FALSE) {
$access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'use domain switcher block'), 'OR');
return $return_as_object ? $access : $access->isAllowed();
}
/**
* Build the output.
*
* @TODO: abstract or theme this function?
*/
public function build() {
/** @var \Drupal\domain\DomainInterface $active_domain */
$active_domain = \Drupal::service('domain.negotiator')->getActiveDomain();
$items = array();
/** @var \Drupal\domain\DomainInterface $domain */
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultipleSorted() as $domain) {
$string = $domain->getLink();
if (!$domain->status()) {
$string .= '*';
}
if ($domain->id() == $active_domain->id()) {
$string = '<em>' . $string . '</em>';
}
$items[] = array('#markup' => $string);
}
return array(
'#theme' => 'item_list',
'#items' => $items,
);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Drupal\domain\Plugin\Block;
use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainInterface;
/**
* Provides a token information block for a domain request.
*
* @Block(
* id = "domain_token_block",
* admin_label = @Translation("Domain token information")
* )
*/
class DomainTokenBlock extends DomainBlockBase {
/**
* Overrides \Drupal\block\BlockBase::access().
*/
public function access(AccountInterface $account, $return_as_object = FALSE) {
$access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'view domain information'), 'OR');
return $return_as_object ? $access : $access->isAllowed();
}
/**
* Build the output.
*
* @TODO: abstract or theme this function?
*/
public function build() {
/** @var \Drupal\domain\DomainInterface $domain */
$domain = \Drupal::service('domain.negotiator')->getActiveDomain();
if (!$domain) {
return array(
'#markup' => $this->t('No domain record could be loaded.'),
);
}
$header = array($this->t('Token'), $this->t('Value'));
return array(
'#theme' => 'table',
'#rows' => $this->renderTokens($domain),
'#header' => $header,
);
}
/**
* Generates available tokens for printing.
*
* @param Drupal\domain\DomainInterface $domain
* The active domain request.
* @return array
* An array keyed by token name, with value of replacement value.
*/
private function renderTokens(DomainInterface $domain) {
$rows = array();
$token = \Drupal::token();
$tokens = $token->getInfo();
// The 'domain' token is supported by core. The others by Token module,
// so we cannot assume that Token module is present.
$domain_tokens = ['domain', 'current-domain', 'default-domain'];
foreach ($domain_tokens as $key) {
if (isset($tokens['tokens'][$key])) {
$data = [];
// Pass domain data to the default handler.
if ($key == 'domain') {
$data['domain'] = $domain;
}
foreach ($tokens['tokens'][$key] as $name => $info) {
$string = "[$key:$name]";
$rows[] = [
$string,
$token->replace($string, $data),
];
}
}
}
return $rows;
}
}
@@ -0,0 +1,145 @@
<?php
namespace Drupal\domain\Plugin\Condition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\domain\DomainNegotiator;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'Domain' condition.
*
* @Condition(
* id = "domain",
* label = @Translation("Domain"),
* context = {
* "entity:domain" = @ContextDefinition("entity:domain", label = @Translation("Domain"), required = FALSE)
* }
* )
*/
class Domain extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* The domain negotiator.
*
* @var \Drupal\domain\DomainNegotiator
*/
protected $domainNegotiator;
/**
* Constructs a Domain condition plugin.
*
* @param \Drupal\domain\DomainNegotiator $domain_negotiator
* The domain negotiator service.
* @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.
*/
public function __construct(DomainNegotiator $domain_negotiator, array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->domainNegotiator = $domain_negotiator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('domain.negotiator'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['domains'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('When the following domains are active'),
'#default_value' => $this->configuration['domains'],
'#options' => array_map('\Drupal\Component\Utility\Html::escape', \Drupal::service('entity_type.manager')->getStorage('domain')->loadOptionsList()),
'#description' => $this->t('If you select no domains, the condition will evaluate to TRUE for all requests.'),
'#attached' => array(
'library' => array(
'domain/drupal.domain',
),
),
);
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return array(
'domains' => array(),
) + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['domains'] = array_filter($form_state->getValue('domains'));
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
// Use the domain labels. They will be sanitized below.
$domains = array_intersect_key(\Drupal::service('entity_type.manager')->getStorage('domain')->loadOptionsList(), $this->configuration['domains']);
if (count($domains) > 1) {
$domains = implode(', ', $domains);
}
else {
$domains = reset($domains);
}
if ($this->isNegated()) {
return $this->t('Active domain is not @domains', array('@domains' => $domains));
}
else {
return $this->t('Active domain is @domains', array('@domains' => $domains));
}
}
/**
* {@inheritdoc}
*/
public function evaluate() {
$domains = $this->configuration['domains'];
if (empty($domains) && !$this->isNegated()) {
return TRUE;
}
// If the context did not load, derive from the request.
if (!$domain = $this->getContextValue('entity:domain')) {
$domain = $this->domainNegotiator->getActiveDomain();
}
// No context found?
if (empty($domain)) {
return FALSE;
}
// NOTE: The context system handles negation for us.
return (bool) in_array($domain->id(), $domains);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$contexts = parent::getCacheContexts();
$contexts[] = 'url.site';
return $contexts;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\domain\Plugin\EntityReferenceSelection;
use Drupal\domain\Plugin\EntityReferenceSelection\DomainSelection;
/**
* Provides entity reference selections for the domain entity type.
*
* @EntityReferenceSelection(
* id = "domain:domain",
* label = @Translation("Domain administrator selection"),
* base_plugin_label = @Translation("Domain administrator"),
* entity_types = {"domain"},
* group = "domain",
* weight = 5
* )
*/
class DomainAdminSelection extends DomainSelection {
/**
* Sets the context for the alter hook.
*
* The only difference between this selector and its parent are the
* permissions used to restrict access. Since the field information is not
* available through the DefaultSelector class, we have to coerce that
* information to pass it to our hook.
*
* We could do this by reading the id from the annotation, but setting an
* explicit variable seems more obvious for developers.
*
* @var string
*/
protected $field_type = 'admin';
}
@@ -0,0 +1,114 @@
<?php
namespace Drupal\domain\Plugin\EntityReferenceSelection;
use Drupal\user\Entity\User;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides entity reference selections for the domain entity type.
*
* @EntityReferenceSelection(
* id = "default:domain",
* label = @Translation("Domain selection"),
* entity_types = {"domain"},
* group = "default",
* weight = 1
* )
*/
class DomainSelection extends DefaultSelection {
/**
* Sets the context for the alter hook.
*
* @var string
*/
protected $field_type = 'editor';
/**
* {@inheritdoc}
*/
public function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Let administrators do anything.
if ($this->currentUser->hasPermission('administer domains')) {
return $query;
}
// Can this user access inactive domains?
if (!$this->currentUser->hasPermission('access inactive domains')) {
$query->condition('status', 1);
}
// Filter domains by the user's assignments, which are controlled by other
// modules. Those modules must know what type of entity they are dealing
// with, so look up the entity type and bundle.
$info = $query->getMetaData('entity_reference_selection_handler');
if (!empty($info->configuration['entity'])) {
$context['entity_type'] = $info->configuration['entity']->getEntityTypeId();
$context['bundle'] = $info->configuration['entity']->bundle();
$context['field_type'] = $this->field_type;
// Load the current user.
$account = User::load($this->currentUser->id());
// Run the alter hook.
$this->moduleHandler->alter('domain_references', $query, $account, $context);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$selection_handler_settings = $this->configuration['handler_settings'];
// Merge-in default values.
$selection_handler_settings += array(
// For the 'target_bundles' setting, a NULL value is equivalent to "allow
// entities from any bundle to be referenced" and an empty array value is
// equivalent to "no entities from any bundle can be referenced".
'target_bundles' => NULL,
'sort' => array(
'field' => 'weight',
'direction' => 'ASC',
),
'auto_create' => FALSE,
'default_selection' => 'current',
);
$form['target_bundles'] = array(
'#type' => 'value',
'#value' => NULL,
);
$fields = array(
'weight' => $this->t('Weight'),
'label' => $this->t('Name'),
'hostname' => $this->t('Hostname'),
);
$form['sort']['field'] = array(
'#type' => 'select',
'#title' => $this->t('Sort by'),
'#options' => $fields,
'#ajax' => FALSE,
'#default_value' => $selection_handler_settings['sort']['field'],
);
$form['sort']['direction'] = array(
'#type' => 'select',
'#title' => $this->t('Sort direction'),
'#required' => TRUE,
'#options' => array(
'ASC' => $this->t('Ascending'),
'DESC' => $this->t('Descending'),
),
'#default_value' => $selection_handler_settings['sort']['direction'],
);
return $form;
}
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\domain\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 Domain source from database.
*
* @MigrateSource(
* id = "d7_domain"
* )
*/
class DomainRecord extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$fields = [
'domain_id',
'subdomain',
'sitename',
'scheme',
'valid',
'weight',
'is_default',
'machine_name',
];
return $this->select('domain', 'd')->fields('d', $fields);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'domain_id' => $this->t('Domain ID.'),
'subdomain' => $this->t('Subdomain.'),
'sitename' => $this->t('Sitename.'),
'scheme' => $this->t('Scheme.'),
'valid' => $this->t('Valid.'),
'weight' => $this->t('Weight.'),
'is_default' => $this->t('Is default.'),
'machine_name' => $this->t('Machine name.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return ['domain_id' => ['type' => 'int']];
}
}
@@ -0,0 +1,189 @@
<?php
namespace Drupal\domain\Plugin\views\access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\domain\DomainNegotiator;
use Drupal\domain\DomainStorageInterface;
use Drupal\views\Plugin\views\access\AccessPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Access plugin that provides domain-based access control.
*
* @ViewsAccess(
* id = "domain",
* title = @Translation("Domain"),
* help = @Translation("Access will be granted when accessed from an allowed domain.")
* )
*/
class Domain extends AccessPluginBase implements CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
protected $usesOptions = TRUE;
/**
* Domain storage.
*
* @var \Drupal\domain\DomainStorageInterface
*/
protected $domainStorage;
/**
* Domain negotiation.
*
* @var \Drupal\domain\DomainNegotiator
*/
protected $domainNegotiator;
/**
* Constructs a Role 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 DomainStorageInterface $domain_storage
* The domain storage loader.
* @param DomainNegotiator $domain_negotiator
* The domain negotiator.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DomainStorageInterface $domain_storage, DomainNegotiator $domain_negotiator) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->domainStorage = $domain_storage;
$this->domainNegotiator = $domain_negotiator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('domain'),
$container->get('domain.negotiator')
);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
$id = $this->domainNegotiator->getActiveId();
$options = array_filter($this->options['domain']);
return isset($options[$id]);
}
/**
* {@inheritdoc}
*/
public function alterRouteDefinition(Route $route) {
if ($this->options['domain']) {
$route->setRequirement('_domain', (string) implode('+', $this->options['domain']));
}
}
/**
* {@inheritdoc}
*/
public function summaryTitle() {
$count = count($this->options['domain']);
if ($count < 1) {
return $this->t('No domain(s) selected');
}
elseif ($count > 1) {
return $this->t('Multiple domains');
}
else {
$domains = $this->domainStorage->loadOptionsList();
$domain = reset($this->options['domain']);
return $domains[$domain];
}
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['domain'] = array('default' => array());
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['domain'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Domain'),
'#default_value' => $this->options['domain'],
'#options' => $this->domainStorage->loadOptionsList(),
'#description' => $this->t('Only the checked domain(s) will be able to access this display.'),
);
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
$domain = $form_state->getValue(array('access_options', 'domain'));
$domain = array_filter($domain);
if (!$domain) {
$form_state->setError($form['domain'], $this->t('You must select at least one domain if type is "by domain"'));
}
$form_state->setValue(array('access_options', 'domain'), $domain);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
foreach (array_keys($this->options['domain']) as $id) {
if ($domain = $this->domainStorage->load($id)) {
$dependencies[$domain->getConfigDependencyKey()][] = $domain->getConfigDependencyName();
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url.site'];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return [];
}
}
@@ -0,0 +1,102 @@
<?php
namespace Drupal\domain\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\domain\DomainInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Component\Utility\Crypt;
use Drupal\user\UserInterface;
use Drupal\Tests\domain\Traits\DomainTestTrait;
/**
* Base class with helper methods and setup for domain tests.
*
* @deprecated
* This class will be removed before the 8.1.0 release.
* Use DomainStorage instead, loaded through the EntityTypeManager.
*/
abstract class DomainTestBase extends WebTestBase {
use DomainTestTrait;
use StringTranslationTrait;
/**
* Sets a base hostname for running tests.
*
* When creating test domains, try to use $this->base_hostname or the
* domainCreateTestDomains() method.
*/
public $base_hostname;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'node');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
}
// Set the base hostname for domains.
$this->setBaseHostname();
}
/**
* Returns whether a given user account is logged in.
*
* @param \Drupal\user\UserInterface $account
* The user account object to check.
*
* @return bool
*/
protected function drupalUserIsLoggedIn($account) {
// @TODO: This is a temporary hack for the test login fails when setting $cookie_domain.
if (!isset($account->session_id)) {
return (bool) $account->id();
}
// The session ID is hashed before being stored in the database.
// @see \Drupal\Core\Session\SessionHandler::read()
return (bool) db_query("SELECT sid FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField();
}
/**
* Login a user on a specific domain.
*
* @param \Drupal\domain\DomainInterface $domain
* The domain to log the user into.
* @param \Drupal\user\UserInterface $account
* The user account to login.
*/
public function domainLogin(DomainInterface $domain, UserInterface $account) {
if ($this->loggedInUser) {
$this->drupalLogout();
}
// Login.
$url = $domain->getPath() . 'user/login';
$edit = ['name' => $account->getAccountName(), 'pass' => $account->passRaw];
$this->drupalPostForm($url, $edit, t('Log in'));
// @see WebTestBase::drupalUserIsLoggedIn()
if (isset($this->sessionId)) {
$account->session_id = $this->sessionId;
}
$pass = $this->assert($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login');
if ($pass) {
$this->loggedInUser = $account;
$this->container->get('current_user')->setAccount($account);
}
}
}
@@ -0,0 +1,32 @@
{#
/**
* @file
* Custom theme implementation for a select-and-go menu list.
*
* Available variables:
* - items: A list of items containing an array of strings.
* - label: the label text
* - url: the link url string
* - active: indicates if the domain is currently active.
*
* @ingroup themeable
*/
#}
{%- if items -%}
<form class="domain-list" action="">
<div class="domain-pointless-validator-class">
<select onchange="if (this.value) location.href=this.value;">
<option value="#">Jump to...</option>
{%- for item in items -%}
{%- set selected = "" -%}
{%- if item.active == true -%}
{%- set selected = "selected" -%}
{%- endif -%}
<option value="{{ item.url }}" {{ selected }} >{{ item.label }}</option>
{%- endfor -%}
</select>
</div>
</form>
{%- else -%}
{{- empty -}}
{%- endif -%}
Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

@@ -0,0 +1,172 @@
langcode: en
status: true
dependencies:
config:
- domain.record.example_com
- domain.record.one_example_com
module:
- domain
- user
id: domain_views_access
label: 'Domain views access'
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: domain
options:
domain:
example_com: example_com
one_example_com: one_example_com
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 1
offset: 0
style:
type: default
row:
type: fields
fields:
name:
id: name
table: users_field_data
field: name
entity_type: user
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: user_name
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: true
table: users_field_data
field: status
plugin_id: boolean
entity_type: user
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts: { }
title: 'Domain views access'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.site
tags: { }
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 2
display_options:
display_extenders: { }
pager:
type: some
options:
items_per_page: 5
offset: 1
defaults:
pager: false
header: false
header: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.site
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: domain-views-access
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.site
tags: { }
@@ -0,0 +1,16 @@
name: "Domain module hook tests"
description: "Support module for domain hook testing."
type: module
package: Testing
# version: VERSION
# core: 8.x
hidden: TRUE
dependencies:
- domain
# Information added by Drupal.org packaging script on 2017-12-19
version: '8.x-1.0-alpha11'
core: '8.x'
project: 'domain'
datestamp: 1513718589
@@ -0,0 +1,60 @@
<?php
/**
* @file
* Domain hook test module.
*/
use Drupal\Core\Url;
use Drupal\domain\DomainInterface;
/**
* Implements hook_domain_load().
*/
function domain_test_domain_load(array $domains) {
foreach ($domains as $domain) {
$domain->addProperty('foo', 'bar');
}
}
/**
* Implements hook_domain_validate_alter().
*/
function domain_test_domain_validate_alter(&$error_list, $hostname) {
// Deliberate test fail.
if ($hostname == 'fail.example.com') {
$error_list[] = 'Fail.example.com cannot be registered';
}
}
/**
* Implements hook_domain_request_alter().
*/
function domain_test_domain_request_alter(DomainInterface &$domain) {
$domain->addProperty('foo1', 'bar1');
}
/**
* Implements hook_domain_operations
*/
function domain_test_domain_operations(DomainInterface $domain) {
$operations = [];
// Add aliases to the list.
$id = $domain->id();
$operations['domain_test'] = array(
'title' => t('Test'),
'url' => Url::fromRoute('entity.domain.edit_form', array('domain' => $id)),
'weight' => 80,
);
return $operations;
}
/**
* Implements hook_domain_references_alter
*/
function domain_test_domain_references_alter($query, $account, $context) {
if ($context['entity_type'] == 'node') {
$test = 'Test string';
$query->addMetadata('domain_test', $test);
}
}
@@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\domain\Functional\Condition;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain condition.
*
* @group domain
*/
class DomainConditionTest extends DomainTestBase {
/**
* The condition plugin manager.
*
* @var \Drupal\Core\Condition\ConditionManager
*/
protected $manager;
/**
* A test domain.
*/
protected $test_domain;
/**
* A test domain that never matches $test_domain.
*/
protected $not_domain;
/**
* An array of all testing domains.
*/
protected $domains;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Set the condition manager.
$this->manager = $this->container->get('plugin.manager.condition');
// Create test domains.
$this->domainCreateTestDomains(5);
// Get two sample domains.
$this->domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
$this->test_domain = array_shift($this->domains);
$this->not_domain = array_shift($this->domains);
}
/**
* Test the domain condition.
*/
public function testConditions() {
// Grab the domain condition and configure it to check against one domain.
$condition = $this->manager->createInstance('domain')
->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id()))
->setContextValue('entity:domain', $this->not_domain);
$this->assertFalse($condition->execute(), 'Domain request condition fails on wrong domain.');
// Grab the domain condition and configure it to check against a null set.
$condition = $this->manager->createInstance('domain')
->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id()))
->setContextValue('entity:domain', NULL);
$this->assertFalse($condition->execute(), 'Domain request condition fails when no context present.');
// Grab the domain condition and configure it to check against itself.
$condition = $this->manager->createInstance('domain')
->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id()))
->setContextValue('entity:domain', $this->test_domain);
$this->assertTrue($condition->execute(), 'Domain request condition succeeds on matching domain.');
// Check for the proper summary.
// Summaries require an extra space due to negate handling in summary().
$this->assertEqual($condition->summary(), 'Active domain is ' . $this->test_domain->label());
// Check the negated summary.
$condition->setConfig('negate', TRUE);
$this->assertEqual($condition->summary(), 'Active domain is not ' . $this->test_domain->label());
// Check the negated condition.
$this->assertFalse($condition->execute(), 'Domain request condition fails when condition negated.');
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain record actions.
*
* @group domain
*/
class DomainActionsTest extends DomainTestBase {
/**
* Tests bulk actions through the domain overview page.
*/
public function testDomainActions() {
$this->admin_user = $this->drupalCreateUser(array('administer domains', 'access administration pages'));
$this->drupalLogin($this->admin_user);
$path = 'admin/config/domain';
// Create test domains.
$this->domainCreateTestDomains(4);
// Visit the domain overview administration page.
$this->drupalGet($path);
$this->assertResponse(200);
// Test the domains.
$storage = \Drupal::service('entity_type.manager')->getStorage('domain');
$domains = $storage->loadMultiple();
$this->assertTrue(count($domains) == 4, 'Four domain records found.');
// Check the default domain.
$default = $storage->loadDefaultId();
$key = 'example_com';
$this->assertTrue($default == $key, 'Default domain set correctly.');
// Test some text on the page.
foreach ($domains as $domain) {
$name = $domain->label();
$this->assertText($name, 'Name found properly.');
}
// Test the list of actions.
$actions = array('delete', 'disable', 'default');
foreach ($actions as $action) {
$this->assertRaw("/domain/{$action}/", 'Actions found properly.');
}
// Check that all domains are active.
$this->assertNoRaw('Inactive', 'Inactive domain not found.');
// Disable a domain and test the enable link.
$this->clickLink('Disable', 0);
$this->assertRaw('Inactive', 'Inactive domain found.');
// Visit the domain overview administration page to clear cache.
$this->drupalGet($path);
$this->assertResponse(200);
foreach ($storage->loadMultiple() as $domain) {
if ($domain->id() == 'one_example_com') {
$this->assertEmpty($domain->status(), 'One domain inactive.');
}
else {
$this->assertNotEmpty($domain->status(), 'Other domains active.');
}
}
// Test the list of actions.
$actions = array('enable', 'delete', 'disable', 'default');
foreach ($actions as $action) {
$this->assertRaw("/domain/{$action}/", 'Actions found properly.');
}
// Re-enable the domain.
$this->clickLink('Enable', 0);
$this->assertNoRaw('Inactive', 'Inactive domain not found.');
// Visit the domain overview administration page to clear cache.
$this->drupalGet($path);
$this->assertResponse(200);
foreach ($storage->loadMultiple() as $domain) {
$this->assertNotEmpty($domain->status(), 'All domains active.');
}
// Set a new default domain.
$this->clickLink('Make default', 0);
// Visit the domain overview administration page to clear cache.
$this->drupalGet($path);
$this->assertResponse(200);
// Check the default domain.
$storage->resetCache();
$default = $storage->loadDefaultId();
$key = 'one_example_com';
$this->assertTrue($default == $key, 'Default domain set correctly.');
}
}
@@ -0,0 +1,142 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests behavior for the domain admin field element.
*
* @group domain
*/
class DomainAdminElementTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'field', 'field_ui', 'user');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create 5 domains.
$this->domainCreateTestDomains(5);
}
/**
* Basic test setup.
*/
public function testDomainAccessElement() {
$admin = $this->drupalCreateUser(array(
'bypass node access',
'administer content types',
'administer users',
'administer domains',
));
$this->drupalLogin($admin);
$this->drupalGet('admin/people/create');
$this->assertSession()->statusCodeEquals(200);
// Create a user through the form.
$this->fillField('name', 'testuser');
$this->fillField('mail', 'test@example.com');
$this->fillField('pass[pass1]', 'test');
$this->fillField('pass[pass2]', 'test');
// We expect to find 5 domain options. We set two as selected.
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
$count = 0;
$ids = ['example_com', 'one_example_com', 'two_example_com'];
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if (in_array($domain->id(), $ids)) {
$this->checkField($locator);
}
}
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
$storage = \Drupal::entityTypeManager()->getStorage('user');
$user = $storage->load(3);
// Check that two values are set.
$manager = \Drupal::service('domain.element_manager');
$values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 3, 'User saved with three domain records.');
// Now login as a user with limited rights.
$account = $this->drupalCreateUser(array(
'administer users',
'assign domain administrators',
));
$ids = ['example_com', 'one_example_com'];
$this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD);
$tester = $storage->load($account->id());
$values = $manager->getFieldValues($tester, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain records.');
$storage->resetCache(array($account->id()));
$this->drupalLogin($account);
$this->drupalGet('user/' . $user->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if ($domain->id() == 'example_com') {
$this->checkField($locator);
}
elseif ($domain->id() == 'one_example_com') {
$this->uncheckField($locator);
}
else {
$this->assertSession()->fieldNotExists($locator);
}
}
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
// Now, check the user.
$storage->resetCache(array($user->id()));
$user = $storage->load($user->id());
// Check that two values are set.
$values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain records.');
// Test the case presented in https://www.drupal.org/node/2841962.
$config = \Drupal::configFactory()->getEditable('user.settings');
$config->set('verify_mail', 0);
$config->set('register', USER_REGISTER_VISITORS);
$config->save();
$this->drupalLogout();
$this->drupalGet('user/register');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseNotContains('Domain administrator');
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->assertSession()->fieldNotExists($locator);
}
// Create a user through the form.
$this->fillField('name', 'testuser2');
$this->fillField('mail', 'test2@example.com');
// In 8.3, this field is not present?
if (!empty($this->findField('pass[pass1]'))) {
$this->fillField('pass[pass1]', 'test');
$this->fillField('pass[pass2]', 'test');
}
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Component\Utility\Html;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain CSS configuration.
*
* @group domain
*/
class DomainCSSTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain');
protected function setUp() {
parent::setUp();
\Drupal::service('theme_handler')->install(array('bartik'));
}
/**
* Tests the handling of an inbound request.
*/
public function testDomainNegotiator() {
// No domains should exist.
$this->domainTableIsEmpty();
// Create four new domains programmatically.
$this->domainCreateTestDomains(4);
// The test runner doesn't use a theme that contains the preprocess hook,
// so set to use Bartik.
$config = $this->config('system.theme');
$config->set('default', 'bartik')->save();
// Test the response of the default home page.
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple() as $domain) {
$this->drupalGet($domain->getPath());
$text = '<body class="' . Html::getClass($domain->id() . '-class');
$this->assertNoRaw($text, 'No custom CSS present.');
}
// Set the css classes.
$config = $this->config('domain.settings');
$config->set('css_classes', '[domain:machine-name]-class')->save();
// Test the response of the default home page.
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple() as $domain) {
// The render cache trips up this test. In production, it may be
// necessary to add the url.site cache context. See README.md.
drupal_flush_all_caches();
$this->drupalGet($domain->getPath());
$text = '<body class="' . Html::getClass($domain->id() . '-class');
$this->assertRaw($text, 'Custom CSS present.' . $text);
}
// Set the css classes.
$config = $this->config('domain.settings');
$config->set('css_classes', '[domain:machine-name]-class [domain:name]-class')->save();
// Test the response of the default home page.
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple() as $domain) {
// The render cache trips up this test. In production, it may be
// necessary to add the url.site cache context. See README.md.
drupal_flush_all_caches();
$this->drupalGet($domain->getPath());
$text = '<body class="' . Html::getClass($domain->id() . '-class') . ' ' . Html::getClass($domain->label() . '-class');
$this->assertRaw($text, 'Custom CSS present.' . $text);
}
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain record response check.
*
* @group domain
*/
class DomainCheckResponseTest extends DomainTestBase {
/**
* Tests that a domain responds as expected.
*/
public function testDomainCheckResponse() {
$this->admin_user = $this->drupalCreateUser(array('administer domains', 'create domains'));
$this->drupalLogin($this->admin_user);
$storage = \Drupal::service('entity_type.manager')->getStorage('domain');
// Make a POST request on admin/config/domain/add.
$edit = $this->domainPostValues();
$this->drupalPostForm('admin/config/domain/add', $edit, 'Save');
// Did it save correctly?
$this->assertNoRaw('The server request to');
$domains = $storage->loadMultiple();
$this->assertTrue(count($domains) == 1, 'Domain record saved via form.');
// Make an invalid POST request on admin/config/domain/add.
$edit = $this->domainPostValues();
// Set a hostname that does not exist on the server.
$edit['hostname'] = 'foo.bar';
$edit['id'] = $storage->createMachineName($edit['hostname']);
$edit['validate_url'] = 1;
try {
$this->drupalPostForm('admin/config/domain/add', $edit, 'Save');
}
catch (Exception $e) {
// Ensure no test errors.
}
// The domain should not save.
$this->assertRaw('The server request to');
$domains = $storage->loadMultiple();
$this->assertTrue(count($domains) == 1, 'Domain record not saved via form.');
// Bypass the check.
$edit['validate_url'] = 0;
$this->drupalPostForm('admin/config/domain/add', $edit, 'Save');
// The domain should save.
$this->assertNoRaw('The server request to');
$domains = $storage->loadMultiple();
$this->assertTrue(count($domains) == 2, 'Domain record saved via form.');
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain record creation API.
*
* @group domain
*/
class DomainCreateTest extends DomainTestBase {
/**
* Tests initial domain creation.
*/
public function testDomainCreate() {
// No domains should exist.
$this->domainTableIsEmpty();
// Create a new domain programmatically.
$storage = \Drupal::service('entity_type.manager')->getStorage('domain');
$domain = $storage->create();
$domain->set('id', $storage->createMachineName($domain->getHostname()));
foreach (array('id', 'name', 'hostname', 'scheme', 'status', 'weight' , 'is_default') as $key) {
$property = $domain->get($key);
$this->assertTrue(isset($property), 'Property loaded');
}
$domain->save();
// Did it save correctly?
$default_id = $storage->loadDefaultId();
$this->assertTrue(!empty($default_id), 'Default domain has been set.');
// Does it load correctly?
$new_domain = $storage->load($default_id);
$this->assertTrue($new_domain->id() == $domain->id(), 'Domain loaded properly.');
// Has domain id been set?
$this->assertTrue($new_domain->getDomainId(), 'Domain id set properly.');
// Has a UUID been set?
$this->assertTrue($new_domain->uuid(), 'Entity UUID set properly.');
// Delete the domain.
$domain->delete();
$domain = $storage->load($default_id, TRUE);
$this->assertTrue(empty($domain), 'Domain record deleted.');
// No domains should exist.
$this->domainTableIsEmpty();
}
}
@@ -0,0 +1,160 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain record entity reference field type.
*
* @group domain
*/
class DomainEntityReferenceTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'field', 'field_ui');
/**
* Create, edit and delete a domain field via the user interface.
*/
public function testDomainField() {
$this->admin_user = $this->drupalCreateUser(array(
'administer content types',
'administer node fields',
'administer node display',
'administer domains',
));
$this->drupalLogin($this->admin_user);
// Visit the article field administration page.
$this->drupalGet('admin/structure/types/manage/article/fields');
$this->assertResponse(200, 'Manage fields page accessed.');
// Check for a domain field.
$this->assertNoText('Domain test field', 'Domain form field not found.');
// Visit the article field display administration page.
$this->drupalGet('admin/structure/types/manage/article/display');
$this->assertResponse(200, 'Manage field display page accessed.');
// Check for a domain field.
$this->assertNoText('Domain test field', 'Domain form field not found.');
// Create test domain field.
$this->domainCreateTestField();
// Visit the article field administration page.
$this->drupalGet('admin/structure/types/manage/article/fields');
// Check the new field.
$this->assertText('Domain test field', 'Added a test field instance.');
// Visit the article field display administration page.
$this->drupalGet('admin/structure/types/manage/article/display');
// Check the new field.
$this->assertText('Domain test field', 'Added a test field display instance.');
}
/**
* Create content for a domain field.
*/
public function testDomainFieldStorage() {
$this->admin_user = $this->drupalCreateUser(array(
'bypass node access',
'administer content types',
'administer node fields',
'administer node display',
'administer domains',
));
$this->drupalLogin($this->admin_user);
// Create test domain field.
$this->domainCreateTestField();
// Create 5 domains.
$this->domainCreateTestDomains(5);
// Visit the article field display administration page.
$this->drupalGet('node/add/article');
$this->assertResponse(200);
// Check the new field exists on the page.
$this->assertText('Domain test field', 'Found the domain field instance.');
// We expect to find 5 domain options.
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
foreach ($domains as $domain) {
$string = 'value="' . $domain->id() . '"';
$this->assertRaw($string, 'Found the domain option');
if (!isset($one)) {
$one = $domain->id();
continue;
}
if (!isset($two)) {
$two = $domain->id();
}
}
// Try to post a node, assigned to the first two domains.
$edit['title[0][value]'] = 'Test node';
$edit["field_domain[{$one}]"] = TRUE;
$edit["field_domain[{$two}]"] = TRUE;
$this->drupalPostForm('node/add/article', $edit, 'Save');
$this->assertResponse(200);
$node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
$values = $node->get('field_domain');
// Get the expected value count.
$this->assertTrue(count($values) == 2, 'Node saved with two domain records.');
}
/**
* Creates a simple field for testing on the article content type.
*
* Note: This code is a model for auto-creation of fields.
*/
public function domainCreateTestField() {
$label = 'domain';
$name = 'field_' . $label;
$storage = array(
'field_name' => $name,
'entity_type' => 'node',
'type' => 'entity_reference',
'cardinality' => -1,
'settings' => array(
'target_type' => 'domain',
),
);
$field_storage_config = \Drupal::entityTypeManager()->getStorage('field_storage_config')->create($storage);
$field_storage_config->save();
$field = array(
'field_name' => $name,
'entity_type' => 'node',
'label' => 'Domain test field',
'bundle' => 'article',
'settings' => array(
'handler_settings' => array(
'sort' => array('field' => 'weight', 'direction' => 'ASC'),
),
),
);
$field_config = \Drupal::entityTypeManager()->getStorage('field_config')->create($field);
$field_config->save();
// Tell the form system how to behave.
entity_get_form_display('node', 'article', 'default')
->setComponent($name, array(
'type' => 'options_buttons',
))
->save();
}
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain record form interface.
*
* @group domain
*/
class DomainFormsTest extends DomainTestBase {
/**
* Create, edit and delete a domain via the user interface.
*/
public function testDomainInterface() {
$this->admin_user = $this->drupalCreateUser(array('administer domains', 'create domains'));
$this->drupalLogin($this->admin_user);
$storage = \Drupal::service('entity_type.manager')->getStorage('domain');
// No domains should exist.
$this->domainTableIsEmpty();
// Visit the main domain administration page.
$this->drupalGet('admin/config/domain');
// Check for the add message.
$this->assertText('There is no Domain record yet.', 'Text for no domains found.');
// Visit the add domain administration page.
$this->drupalGet('admin/config/domain/add');
// Make a POST request on admin/config/domain/add.
$edit = $this->domainPostValues();
$this->drupalPostForm('admin/config/domain/add', $edit, 'Save');
// Did it save correctly?
$default_id = $storage->loadDefaultId();
$this->assertTrue(!empty($default_id), 'Domain record saved via form.');
// Does it load correctly?
$storage->resetCache([$default_id]);
$new_domain = $storage->load($default_id);
$this->assertTrue($new_domain->id() == $default_id, 'Domain loaded properly.');
// Has a UUID been set?
$this->assertTrue(!empty($new_domain->uuid()), 'Entity UUID set properly.');
// Visit the edit domain administration page.
$editUrl = 'admin/config/domain/edit/' . $new_domain->id();
$this->drupalGet($editUrl);
// Update the record.
$edit = [];
$edit['name'] = 'Foo';
$edit['validate_url'] = 0;
$this->drupalPostForm($editUrl, $edit, 'Save');
// Check that the update succeeded.
$storage->resetCache([$default_id]);
$domain = $storage->load($default_id);
$this->assertTrue($domain->label() == 'Foo', 'Domain record updated via form.');
// Visit the delete domain administration page.
$deleteUrl = 'admin/config/domain/delete/' . $new_domain->id();
$this->drupalGet($deleteUrl);
// Delete the record.
$this->drupalPostForm($deleteUrl, array(), 'Delete');
$storage->resetCache([$default_id]);
$domain = $storage->load($default_id);
$this->assertTrue(empty($domain), 'Domain record deleted.');
// No domains should exist.
$this->domainTableIsEmpty();
}
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests domain record HTTP response.
*
* @group domain
*/
class DomainGetResponseTest extends DomainTestBase {
/**
* Tests that a domain response is proper.
*/
public function testDomainResponse() {
// No domains should exist.
$this->domainTableIsEmpty();
// Create a new domain programmatically.
$this->domainCreateTestDomains();
// Check the created domain based on it's known id value.
$key = 'example_com';
/** @var \Drupal\domain\Entity\Domain $domain */
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->load($key);
// Our testing server should be able to access the test PNG file.
$this->assertTrue($domain->getResponse() == 200, 'Server returned a 200 response.');
// Now create a bad domain.
$values = array(
'hostname' => 'foo.bar',
'id' => 'foo_bar',
'name' => 'Foo',
);
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->create($values);
$domain->save();
$this->assertTrue($domain->getResponse() == 500, 'Server test returned a 500 response.');
}
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\RoleInterface;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the access rules and redirects for inactive domains.
*
* @group domain
*/
class DomainInactiveTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'node', 'views');
/**
* Test inactive domain.
*/
public function testInactiveDomain() {
// Configure 'node' as front page, else the test loads the login form.
$site_config = $this->config('system.site');
$site_config->set('page.front', '/node')->save();
// Create four new domains programmatically.
$this->domainCreateTestDomains(4);
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
// Grab a known domain for testing.
$domain = $domains['two_example_com'];
$this->drupalGet($domain->getPath());
$this->assertTrue($domain->status(), 'Tested domain is set to active.');
$this->assertTrue($domain->getPath() == $this->getUrl(), 'Loaded the active domain.');
// Disable the domain and test for redirect.
$domain->disable();
$default = \Drupal::service('entity_type.manager')->getStorage('domain')->loadDefaultDomain();
// Our postSave() cache tag clear should allow this to work properly.
$this->drupalGet($domain->getPath());
$this->assertFalse($domain->status(), 'Tested domain is set to inactive.');
$this->assertTrue($default->getPath() == $this->getUrl(), 'Redirected an inactive domain to the default domain.');
// Check to see if the user can login.
$url = $domain->getPath() . 'user/login';
$this->drupalGet($url);
$this->assertResponse(200, 'Request to login on inactive domain allowed.');
// Check to see if the user can reset password.
$url = $domain->getPath() . 'user/password';
$this->drupalGet($url);
$this->assertResponse(200, 'Request to reset password on inactive domain allowed.');
// Try to access with the proper permission.
user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('access inactive domains'));
// Must flush cache because we did not resave the domain.
drupal_flush_all_caches();
$this->assertFalse($domain->status(), 'Tested domain is set to inactive.');
$this->drupalGet($domain->getPath());
// Set up two additional domains.
$domain2 = $domains['one_example_com'];
$domain3 = $domains['three_example_com'];
// Check against trusted host patterns.
$settings['settings']['trusted_host_patterns'] = (object) [
'value' => ['^' . $this->prepareTrustedHostname($domain->getHostname()) . '$',
'^' . $this->prepareTrustedHostname($domain2->getHostname()) . '$'],
'required' => TRUE,
];
$this->writeSettings($settings);
// Revoke the permission change.
user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access inactive domains'));
$domain2->saveDefault();
// Test the trusted host, which should redirect to default.
$this->drupalGet($domain->getPath());
$this->assertTrue($domain2->getPath() == $this->getUrl(), 'Redirected from the inactive domain.');
$this->assertResponse(200, 'Request to trusted host allowed.');
// The redirect is cached, so we must flush cache to test again.
drupal_flush_all_caches();
// Test another inactive domain that is not trusted.
// Disable the domain and test for redirect.
$domain3->saveDefault();
$this->drupalGet($domain->getPath());
$this->assertRaw('The provided host name is not a valid redirect.');
}
}
@@ -0,0 +1,213 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests behavior for the domain list builder.
*
* @group domain
*/
class DomainListBuilderTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'user');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create 150 domains.
$this->domainCreateTestDomains(150);
}
/**
* Basic test setup.
*/
public function testDomainListBuilder() {
$admin = $this->drupalCreateUser(array(
'bypass node access',
'administer content types',
'administer node fields',
'administer node display',
'administer domains',
));
$this->drupalLogin($admin);
$this->drupalGet('admin/config/domain');
$this->assertSession()->statusCodeEquals(200);
// Check that links are printed.
foreach ($this->getPaginatedDomains() as $domain) {
$href = 'admin/config/domain/edit/' . $domain->id();
$this->assertSession()->linkByHrefExists($href, 0, 'Link found ' . $href);
$this->assertSession()->assertEscaped($domain->label());
// Check for pagination.
$this->checkPagination();
}
// Go to page 2.
$this->clickLink('Next');
foreach ($this->getPaginatedDomains(1) as $domain) {
$href = 'admin/config/domain/edit/' . $domain->id();
$this->assertSession()->linkByHrefExists($href, 0, 'Link found ' . $href);
$this->assertSession()->assertEscaped($domain->label());
// Check for pagination.
$this->checkPagination();
}
// Now login as a user with limited rights.
$account = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'edit assigned domains',
'view domain list',
));
$ids = ['example_com', 'one_example_com'];
$this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD);
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
$user = $user_storage->load($account->id());
$manager = \Drupal::service('domain.element_manager');
$values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain records.');
$this->drupalLogin($account);
$this->drupalGet('admin/config/domain');
$this->assertSession()->statusCodeEquals(200);
// Check that links are printed.
$path = 'admin/config/domain';
$this->drupalGet($path);
foreach ($this->getPaginatedDomains() as $domain) {
$href = 'admin/config/domain/edit/' . $domain->id();
if (in_array($domain->id(), $ids)) {
$this->assertSession()->linkByHrefExists($href, 0, 'Link found');
$this->assertSession()->assertEscaped($domain->label());
}
else {
$this->assertSession()->linkByHrefNotExists($href, 'Link not found');
$this->assertSession()->assertEscaped($domain->label());
}
// Check for pagination.
$this->checkPagination();
}
// Check access to the pages/routes.
foreach ($this->getPaginatedDomains() as $domain) {
$path = 'admin/config/domain/edit/' . $domain->id();
$this->drupalGet($path);
if (in_array($domain->id(), $ids)) {
$this->assertSession()->statusCodeEquals(200);
}
else {
$this->assertSession()->statusCodeEquals(403);
}
}
// Go to page 2.
$this->drupalGet('admin/config/domain');
$this->clickLink('Next');
foreach ($this->getPaginatedDomains(1) as $domain) {
$href = 'admin/config/domain/edit/' . $domain->id();
if (in_array($domain->id(), $ids)) {
$this->assertSession()->linkByHrefExists($href, 0, 'Link found');
$this->assertSession()->assertEscaped($domain->label());
}
else {
$this->assertSession()->linkByHrefNotExists($href, 'Link not found');
$this->assertSession()->assertEscaped($domain->label());
}
// Check for pagination.
$this->checkPagination();
}
// Now login as a user with more limited rights.
$account2 = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'edit assigned domains',
'view assigned domains',
));
$ids = ['example_com', 'one_example_com'];
$this->addDomainsToEntity('user', $account2->id(), $ids, DOMAIN_ADMIN_FIELD);
$user_storage = \Drupal::entityTypeManager()->getStorage('user');
$user = $user_storage->load($account2->id());
$manager = \Drupal::service('domain.element_manager');
$values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain records.');
$this->drupalLogin($account2);
$this->drupalGet('admin/config/domain');
$this->assertSession()->statusCodeEquals(200);
// Check that domains are listed and links are printed.
$path = 'admin/config/domain';
$this->drupalGet($path);
foreach ($this->getPaginatedDomains() as $domain) {
$href = 'admin/config/domain/edit/' . $domain->id();
if (in_array($domain->id(), $ids)) {
$this->assertSession()->linkByHrefExists($href, 0, 'Link found');
$this->assertSession()->assertEscaped($domain->label());
}
else {
$this->assertSession()->linkByHrefNotExists($href, 'Link not found');
$this->assertSession()->assertNoEscaped($domain->label());
}
// Check for pagination.
$this->checkNoPagination();
}
// Check access to the pages/routes.
foreach ($this->getPaginatedDomains() as $domain) {
$path = 'admin/config/domain/edit/' . $domain->id();
$this->drupalGet($path);
if (in_array($domain->id(), $ids)) {
$this->assertSession()->statusCodeEquals(200);
}
else {
$this->assertSession()->statusCodeEquals(403);
}
}
}
/**
* Returns an array of domains, paginated and sorted by weight.
*
* @param int $page
* The page number to return.
*/
private function getPaginatedDomains($page = 0) {
$limit = 50;
$offset = $page * $limit;
return array_slice($this->getDomainsSorted(), $offset, $limit);
}
/**
* Checks that pagination links appear, as expected.
*/
private function checkPagination() {
foreach (['?page=0', '?page=1', '?page=2'] as $href) {
$this->assertSession()->linkByHrefExists($href, 0, 'Link found');
}
}
/**
* Checks that pagination links do not appear, as expected.
*/
private function checkNoPagination() {
foreach (['?page=0', '?page=1', '?page=2'] as $href) {
$this->assertSession()->linkByHrefNotExists($href, 0, 'Link not found');
}
}
}
@@ -0,0 +1,110 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests behavior for the weight element of the domain list builder.
*
* @group domain
*/
class DomainListWeightTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'user');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create 60 domains. We paginate at 50.
$this->domainCreateTestDomains(60);
}
/**
* Basic test setup.
*/
public function testDomainWeight() {
// Test the default sort values. Should be 1 to 60.
$domains = $this->getDomainsSorted();
$i = 1;
foreach ($domains as $domain) {
$this->assert($domain->getWeight() == $i, 'Weight set to ' . $i);
$i++;
}
// The last domain should be test59_example_com.
$this->assert($domain->id() == 'test59_example_com', 'Last domain is test59');
$domains_old = $domains;
$admin = $this->drupalCreateUser(array(
'bypass node access',
'administer content types',
'administer node fields',
'administer node display',
'administer domains',
));
$this->drupalLogin($admin);
$this->drupalGet('admin/config/domain');
$this->assertSession()->statusCodeEquals(200);
// Set one weight to 61.
$locator = 'edit-domains-one-example-com-weight';
$this->fillField($locator, 61);
// Save the form.
$this->pressButton('edit-submit');
$domains = $this->getDomainsSorted();
$i = 1;
foreach ($domains as $domain) {
// Weights should be the same one page 1 except for the one we changed.
if ($domain->id() == 'one_example_com') {
$this->assert($domain->getWeight() == 61, 'Weight set to 61 ' . $domain->getWeight());
}
else {
$this->assert($domain->getWeight() == $domains_old[$domain->id()]->getWeight(). 'Weights unchanged');
}
$i++;
}
// The last domain should be one_example_com.
$this->assert($domain->id() == 'one_example_com', 'Last domain is one');
// Go to page two.
$this->clickLink('Next');
$this->assertSession()->statusCodeEquals(200);
// Set one weight to 2.
$locator = 'edit-domains-one-example-com-weight';
$this->fillField($locator, 2);
// Save the form.
$this->pressButton('edit-submit');
$this->drupalGet('admin/config/domain');
$this->assertSession()->statusCodeEquals(200);
// Go to page two.
$this->clickLink('Next');
$this->assertSession()->statusCodeEquals(200);
// Check the domain sort order.
$domains = $this->getDomainsSorted();
$i = 1;
foreach ($domains as $domain) {
if ($domain->id() == 'one_example_com') {
$this->assert($domain->getWeight() == 2, 'Weight set to 2');
}
else {
$this->assert($domain->getWeight() == $domains_old[$domain->id()]->getWeight(). 'Weights unchanged');
}
}
// The last domain should be test59_example_com.
$this->assert($domain->id() == 'test59_example_com', 'Last domain is test59' . $domain->id());
}
}
@@ -0,0 +1,97 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain navigation block.
*
* @group domain
*/
class DomainNavBlockTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'node', 'block');
/**
* Test domain navigation block.
*/
public function testDomainNav() {
// Create four new domains programmatically.
$this->domainCreateTestDomains(4);
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
// Place the nav block.
$block = $this->drupalPlaceBlock('domain_nav_block');
// Let the anon user view the block.
user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('use domain nav block'));
// Load the homepage. All links should appear.
$this->drupalGet('<front>');
// Confirm domain links.
foreach ($domains as $id => $domain) {
$this->findLink($domain->label());
}
// Disable one of the domains. One link should not appear.
$disabled = $domains['one_example_com'];
$disabled->disable();
// Load the homepage.
$this->drupalGet('<front>');
// Confirm domain links.
foreach ($domains as $id => $domain) {
if ($id != 'one_example_com') {
$this->findLink($domain->label());
}
else {
$this->assertNoRaw($domain->label());
}
}
// Let the anon user view diabled domains. All links should appear.
user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('access inactive domains'));
// Load the homepage.
$this->drupalGet('<front>');
// Confirm domain links.
foreach ($domains as $id => $domain) {
$this->findLink($domain->label());
}
// Now update the configuration and test again.
$this->config('block.block.' . $block->id())
->set('settings.link_options', 'active')
->set('settings.link_label', 'hostname')
->save();
// Load the the login page.
$this->drupalGet('user/login');
// Confirm domain links.
foreach ($domains as $id => $domain) {
$this->findLink($domain->getHostname());
$this->assertRaw($domain->buildUrl('/user/login'));
}
// Now update the configuration and test again.
$this->config('block.block.' . $block->id())
->set('settings.link_options', 'home')
->set('settings.link_theme', 'menu')
->set('settings.link_label', 'url')
->save();
// Load the the login page.
$this->drupalGet('user/login');
// Confirm domain links.
foreach ($domains as $id => $domain) {
$this->findLink($domain->getPath());
$this->assertRaw($domain->getPath());
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain negotiation manager.
*
* @group domain
*/
class DomainNegotiatorTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'domain_test', 'block');
/**
* Tests the handling of an inbound request.
*/
public function testDomainNegotiator() {
// No domains should exist.
$this->domainTableIsEmpty();
// Create four new domains programmatically.
$this->domainCreateTestDomains(4);
// Since we cannot read the service request, we place a block
// which shows the current domain information.
$this->drupalPlaceBlock('domain_server_block');
// To get around block access, let the anon user view the block.
user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('view domain information'));
// Test the response of the default home page.
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple() as $domain) {
$this->drupalGet($domain->getPath());
$this->assertRaw($domain->label(), 'Loaded the proper domain.');
}
}
}
@@ -0,0 +1,216 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests behavior for hook_domain_references_alter().
*
* The module suite ships with two field types -- admin and editor. We want to ensure
* that these are filtered properly by hook_domain_references_alter().
*
* @group domain
*/
class DomainReferencesTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'domain_access', 'field', 'field_ui', 'user');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create 5 domains.
$this->domainCreateTestDomains(5);
}
/**
* Basic test setup.
*/
public function testDomainReferences() {
// Create an admin user. This will be user 2.
$admin = $this->drupalCreateUser(array(
'bypass node access',
'administer content types',
'administer users',
'administer domains',
'assign domain editors',
));
$this->drupalLogin($admin);
$this->drupalGet('admin/people/create');
$this->assertSession()->statusCodeEquals(200);
// Create a user through the form. This will be user 3.
$this->fillField('name', 'testuser');
$this->fillField('mail', 'test@example.com');
$this->fillField('pass[pass1]', 'test');
$this->fillField('pass[pass2]', 'test');
// We expect to find 5 domain options. We set three as selected.
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
$ids = ['example_com', 'one_example_com', 'two_example_com'];
$edit_ids = ['example_com', 'one_example_com'];
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if (in_array($domain->id(), $ids)) {
$this->checkField($locator);
}
$locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if (in_array($domain->id(), $edit_ids)) {
$this->checkField($locator);
}
}
// Find the all affiliates field.
$locator = DOMAIN_ACCESS_ALL_FIELD . '[value]';
$this->findField($locator);
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
// Load our test user.
$storage = \Drupal::entityTypeManager()->getStorage('user');
$testuser = $storage->load(3);
// Check that three values are set.
$manager = \Drupal::service('domain.element_manager');
$values = $manager->getFieldValues($testuser, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 3, 'User saved with three domain admin records.');
// Check that no access fields are set.
$values = $manager->getFieldValues($testuser, DOMAIN_ACCESS_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain access records.');
// Now login as a user with limited rights. This is user 4.
$account = $this->drupalCreateUser(array(
'administer users',
'assign domain administrators',
));
// Set some domain assignments for this user.
$ids = ['example_com', 'one_example_com'];
$this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD);
$limited_admin = $storage->load($account->id());
$values = $manager->getFieldValues($limited_admin, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain admin records.');
// Check that no access fields are set.
$values = $manager->getFieldValues($limited_admin, DOMAIN_ACCESS_FIELD);
$this->assert(count($values) == 0, 'User saved with no domain access records.');
// Now edit user 3 as user 4 with limited rights.
$this->drupalLogin($account);
$this->drupalGet('user/' . $testuser->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if ($domain->id() == 'example_com') {
$this->checkField($locator);
}
elseif ($domain->id() == 'one_example_com') {
$this->uncheckField($locator);
}
else {
$this->assertSession()->fieldNotExists($locator);
}
// No Domain Access field rights exist for this user.
$locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']';
$this->assertSession()->fieldNotExists($locator);
}
// The all affiliates field should not be present..
$locator = DOMAIN_ACCESS_ALL_FIELD . '[value]';
$this->assertSession()->fieldNotExists($locator);
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
// Now, check the user.
$storage->resetCache(array($testuser->id()));
$testuser = $storage->load($testuser->id());
// Check that two values are set.
$values = $manager->getFieldValues($testuser, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain admin records.');
// Check that no access fields are set.
$values = $manager->getFieldValues($testuser, DOMAIN_ACCESS_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain access records.');
// Now login as a user with different limited rights. This is user 5.
$new_account = $this->drupalCreateUser(array(
'administer users',
'assign domain administrators',
'assign domain editors',
));
$ids = ['example_com', 'one_example_com'];
$new_ids = ['one_example_com', 'four_example_com'];
$this->addDomainsToEntity('user', $new_account->id(), $ids, DOMAIN_ADMIN_FIELD);
$this->addDomainsToEntity('user', $new_account->id(), $new_ids, DOMAIN_ACCESS_FIELD);
$new_admin = $storage->load($new_account->id());
$values = $manager->getFieldValues($new_admin, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain admin records.');
$values = $manager->getFieldValues($new_admin, DOMAIN_ACCESS_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain access records.');
// Now edit the user as someone with limited rights.
$storage->resetCache(array($new_admin->id()));
$this->drupalLogin($new_account);
$this->drupalGet('user/' . $testuser->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
foreach ($domains as $domain) {
$locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']';
$this->findField($locator);
if ($domain->id() == 'example_com') {
$this->checkField($locator);
}
elseif ($domain->id() == 'one_example_com') {
$this->uncheckField($locator);
}
else {
$this->assertSession()->fieldNotExists($locator);
}
// Some Domain Access field rights exist for this user. This adds
// one to the count.
$locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']';
if (in_array($domain->id(), $new_ids)) {
$this->findField($locator);
$this->checkField($locator);
}
else {
$this->assertSession()->fieldNotExists($locator);
}
}
// The all affiliates field should not be present..
$locator = DOMAIN_ACCESS_ALL_FIELD . '[value]';
$this->assertSession()->fieldNotExists($locator);
// Save the form.
$this->pressButton('edit-submit');
$this->assertSession()->statusCodeEquals(200);
// Now, check the user.
$storage->resetCache(array($testuser->id()));
$testuser = $storage->load($testuser->id());
// Check that two values are set.
$values = $manager->getFieldValues($testuser, DOMAIN_ADMIN_FIELD);
$this->assert(count($values) == 2, 'User saved with two domain admin records.');
$values = $manager->getFieldValues($testuser, DOMAIN_ACCESS_FIELD);
$this->assert(count($values) == 3, 'User saved with three domain access records.');
}
}
@@ -0,0 +1,218 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\Tests\BrowserTestBase;
use Drupal\domain\DomainInterface;
use Drupal\Tests\domain\Traits\DomainTestTrait;
abstract class DomainTestBase extends BrowserTestBase {
use DomainTestTrait;
/**
* Sets a base hostname for running tests.
*
* When creating test domains, try to use $this->base_hostname or the
* domainCreateTestDomains() method.
*/
public $base_hostname;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'node');
/**
* We use the standard profile for testing.
*
* @var string
*/
protected $profile = 'standard';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Set the base hostname for domains.
$this->base_hostname = \Drupal::service('entity_type.manager')->getStorage('domain')->createHostname();
}
/**
* The methods below are brazenly copied from Rules module. They are all
* helper methods that make writing tests a bit easier.
*/
/**
* Finds link with specified locator.
*
* @param string $locator
* Link id, title, text or image alt.
*
* @return \Behat\Mink\Element\NodeElement|null
* The link node element.
*/
public function findLink($locator) {
return $this->getSession()->getPage()->findLink($locator);
}
/**
* Confirms absence of link with specified locator.
*
* @param string $locator
* Link id, title, text or image alt.
*
* @return \Behat\Mink\Element\NodeElement|null
* The link node element.
*/
public function findNoLink($locator) {
return empty($this->getSession()->getPage()->hasLink($locator));
}
/**
* Finds field (input, textarea, select) with specified locator.
*
* @param string $locator
* Input id, name or label.
*
* @return \Behat\Mink\Element\NodeElement|null
* The input field element.
*/
public function findField($locator) {
return $this->getSession()->getPage()->findField($locator);
}
/**
* Finds button with specified locator.
*
* @param string $locator
* Button id, value or alt.
*
* @return \Behat\Mink\Element\NodeElement|null
* The button node element.
*/
public function findButton($locator) {
return $this->getSession()->getPage()->findButton($locator);
}
/**
* Presses button with specified locator.
*
* @param string $locator
* Button id, value or alt.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
public function pressButton($locator) {
$this->getSession()->getPage()->pressButton($locator);
}
/**
* Fills in field (input, textarea, select) with specified locator.
*
* @param string $locator
* Input id, name or label.
* @param string $value
* Value.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*
* @see \Behat\Mink\Element\NodeElement::setValue
*/
public function fillField($locator, $value) {
$this->getSession()->getPage()->fillField($locator, $value);
}
/**
* Checks checkbox with specified locator.
*
* @param string $locator input id, name or label
*
* @throws ElementNotFoundException
*/
public function checkField($locator) {
$this->getSession()->getPage()->checkField($locator);
}
/**
* Unchecks checkbox with specified locator.
*
* @param string $locator input id, name or label
*
* @throws ElementNotFoundException
*/
public function uncheckField($locator) {
$this->getSession()->getPage()->uncheckField($locator);
}
/**
* Selects option from select field with specified locator.
*
* @param string $locator input id, name or label
* @param string $value option value
* @param Boolean $multiple select multiple options
*
* @throws ElementNotFoundException
*
* @see NodeElement::selectOption
*/
public function selectFieldOption($locator, $value, $multiple = false) {
$this->getSession()->getPage()->selectFieldOption($locator, $value, $multiple);
}
/**
* Returns whether a given user account is logged in.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account object to check.
*
* @return bool
*/
protected function drupalUserIsLoggedIn(AccountInterface $account) {
// @TODO: This is a temporary hack for the test login fails when setting $cookie_domain.
if (!isset($account->session_id)) {
return (bool) $account->id();
}
// The session ID is hashed before being stored in the database.
// @see \Drupal\Core\Session\SessionHandler::read()
return (bool) db_query("SELECT sid FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField();
}
/**
* Login a user on a specific domain.
*
* @param \Drupal\domain\DomainInterface $domain
* The domain to log the user into.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account to login.
*/
public function domainLogin(DomainInterface $domain, AccountInterface $account) {
// Due to a quirk in session handling that we cannot directly access, it
// works if we login, then logout, and then login to a specific domain.
$this->drupalLogin($account);
if ($this->loggedInUser) {
$this->drupalLogout();
}
// Login.
$url = $domain->getPath() . 'user/login';
$this->submitForm([
'name' => $account->getUsername(),
'pass' => $account->passRaw,
], t('Log in'));
// @see BrowserTestBase::drupalUserIsLoggedIn()
$account->sessionId = $this->getSession()->getCookie($this->getSessionName());
$this->assertTrue($this->drupalUserIsLoggedIn($account), 'User successfully logged in.');
$this->loggedInUser = $account;
$this->container->get('current_user')->setAccount($account);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain token handler.
*
* @group domain
*/
class DomainTokenTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'block');
/**
* Tests the handling of an inbound request.
*/
public function testDomainTokens() {
// No domains should exist.
$this->domainTableIsEmpty();
// Create four new domains programmatically.
$this->domainCreateTestDomains(4);
// Since we cannot read the service request, we place a block
// which shows the current domain token information.
$this->drupalPlaceBlock('domain_token_block');
// To get around block access, let the anon user view the block.
user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('view domain information'));
// Test the response of the default home page.
foreach (\Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple() as $domain) {
$this->drupalGet($domain->getPath());
$this->assertRaw($domain->label(), 'Loaded the proper domain.');
$this->assertRaw('<th>Token</th>', 'Token values printed.');
foreach ($this->tokenList() as $token => $callback) {
$this->assertRaw("<td>$token</td>", "$token found correctly.");
// The URL token is sensitive to the path, which is /user, but that
// does not come across when making the callback outside of a request
// context.
$value = $domain->{$callback}();
if ($token == '[domain:url]') {
$value = str_replace('user', '', $value);
if (substr($value, -1) != '/') {
$value .= '/';
}
}
$this->assertRaw('<td>' . $value . '</td>', 'Value set correctly to ' . $value);
}
}
}
/**
* Gets the list of tokens and value callbacks used by the test.
*
* @return array
* An array keyed by token string, with value of expected domain value.
*/
private function tokenList() {
$tokens = [];
foreach (\Drupal::service('domain.token')->getCallbacks() as $key => $callback) {
$name = "[domain:$key]";
$tokens[$name] = $callback;
}
return $tokens;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Core\Config\ConfigValueException;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests domain record validation.
*
* @group domain
*/
class DomainValidatorTest extends DomainTestBase {
/**
* Tests that a domain hostname validates.
*/
public function testDomainValidator() {
// No domains should exist.
$this->domainTableIsEmpty();
$validator = \Drupal::service('domain.validator');
$storage = \Drupal::service('entity_type.manager')->getStorage('domain');
// Create a domain.
$this->domainCreateTestDomains(1, 'foo.com');
// Check the created domain based on its known id value.
$key = 'foo.com';
/** @var \Drupal\domain\Entity\Domain $domain */
$domain = $storage->loadByHostname($key);
$this->assertTrue(!empty($domain), 'Test domain created.');
// Valid hostnames to test. Valid is the boolean value.
$hostnames = [
'localhost' => 1,
'example.com' => 1,
'www.example.com' => 1, // see www-prefix test, below.
'one.example.com' => 1,
'example.com:8080' => 1,
'example.com::8080' => 0, // only one colon.
'example.com:abc' => 0, // no letters after a colon.
'.example.com' => 0, // cannot begin with a dot.
'example.com.' => 0, // cannot end with a dot.
'EXAMPLE.com' => 0, // lowercase only.
'éxample.com' => 0, // ascii-only.
];
foreach ($hostnames as $hostname => $valid) {
$errors = $validator->validate($hostname);
if ($valid) {
$this->assertTrue(empty($errors), 'Validation correct with no errors.');
}
else {
$this->assertTrue(!empty($errors), 'Validation correct with proper errors.');
}
}
// Test duplicate hostname creation.
$test_hostname = 'foo.com';
$test_domain = $storage->create([
'hostname' => $test_hostname,
'name' => 'Test domain',
'id' => 'test_domain',
]);
try {
$test_domain->save();
$this->fail('Duplicate hostname validation');
}
catch (ConfigValueException $e) {
$expected_message = "The hostname ($test_hostname) is already registered.";
$this->assertEqual($expected_message, $e->getMessage());
}
// Test the two configurable options.
$config = $this->config('domain.settings');
$config->set('www_prefix', true);
$config->set('allow_non_ascii', true);
$config->save();
// Valid hostnames to test. Valid is the boolean value.
$hostnames = [
'www.example.com' => 0, // no www-prefix allowed
'éxample.com' => 1, // ascii-only allowed.
];
foreach ($hostnames as $hostname => $valid) {
$errors = $validator->validate($hostname);
if ($valid) {
$this->assertTrue(empty($errors),'Validation test correct with no errors.');
}
else {
$this->assertTrue(!empty($errors), 'Validation test correct with errors.');
}
}
}
}
@@ -0,0 +1,84 @@
<?php
namespace Drupal\Tests\domain\Functional;
use Drupal\Tests\domain\Functional\DomainTestBase;
/**
* Tests the domain access plugin for Views.
*
* @group domain
*/
class DomainViewsAccessTest extends DomainTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'node', 'views', 'block');
/**
* Disabled config schema checking.
*
* @TODO: https://github.com/agentrickard/domain/issues/200
*/
protected $strictConfigSchema = FALSE;
/**
* Test inactive domain.
*/
public function testInactiveDomain() {
// Create five new domains programmatically.
$this->domainCreateTestDomains(5);
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
// Enable the views.
$this->enableViewsTestModule();
// Create a user. To test the area output was more difficult, so we just
// configured two views. The page shows the first, admin, user, and the
// block will show this new user name.
$this->user = $this->drupalCreateUser(array('administer domains', 'create domains'));
// Place the view block.
$this->drupalPlaceBlock('views_block:domain_views_access-block_1');
// The block and page should be visible on example_com and one_example_com.
$allowed = ['example_com', 'one_example_com'];
foreach ($domains as $domain) {
$path = $domain->getPath() . 'domain-views-access';
$this->DrupalGet($path);
if (in_array($domain->id(), $allowed)) {
$this->assertResponse('200', 'Access allowed');
$this->assertRaw('admin');
$this->assertRaw($this->user->getUsername());
}
else {
$this->assertResponse('403', 'Access denied');
$this->assertNoRaw('admin');
$this->assertNoRaw($this->user->getUsername());
}
// Test the block on another page.
$this->drupalGet($domain->getPath());
if (in_array($domain->id(), $allowed)) {
$this->assertRaw($this->user->getUsername());
}
else {
$this->assertNoRaw($this->user->getUsername());
}
}
}
/**
* Sets up the domain_test module.
*
* Because the schema of domain_test.module is dependent on the test
* using it, it cannot be enabled normally.
*/
protected function enableViewsTestModule() {
\Drupal::service('module_installer')->install(array('domain_test'));
$this->resetAll();
$this->rebuildContainer();
$this->container->get('module_handler')->reload();
}
}
@@ -0,0 +1,160 @@
<?php
namespace Drupal\Tests\domain\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\domain\Traits\DomainTestTrait;
/**
* Tests domain hooks documented in domain.api.php.
*
* Note that the other hooks are covered by functional tests, since they involve UI
* elements.
*
* @see DomainReferencesTest
* @see DomainListBuilderTes
* @see DomainAliasNegotiatorTest
*
* @group domain
*/
class DomainHookTest extends KernelTestBase {
use DomainTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain', 'domain_test', 'user', 'node');
/**
* Domain id key.
*/
public $key = 'example_com';
/**
* The Domain storage handler service.
*/
public $domainStorage;
/**
* The current user service.
*/
public $currentUser;
/**
* The mondule handler service.
*/
public $moduleHandler;
/**
* Test setup.
*/
protected function setUp() {
parent::setUp();
// Create a domain.
$this->domainCreateTestDomains();
// Get the services.
$this->domainStorage = \Drupal::service('entity_type.manager')->getStorage('domain');
$this->currentUser = \Drupal::service('current_user');
$this->moduleHandler = \Drupal::service('module_handler');
}
/**
* Tests domain loading.
*/
public function testHookDomainLoad() {
// Check the created domain based on its known id value.
$domain = $this->domainStorage->load($this->key);
// Internal hooks.
$path = $domain->getPath();
$url = $domain->getUrl();
$this->assertTrue(isset($path), new FormattableMarkup('The path property was set to %path by hook_entity_load.', array('%path' => $path)));
$this->assertTrue(isset($url), new FormattableMarkup('The url property was set to %url by hook_entity_load.', array('%url' => $url)));
// External hooks.
$this->assertTrue($domain->foo == 'bar', 'The foo property was set to <em>bar</em> by hook_domain_load.');
}
/**
* Tests domain validation.
*/
public function testHookDomainValidate() {
$validator = \Drupal::service('domain.validator');
// Test a good domain.
$errors = $validator->validate('one.example.com');
$this->assertEmpty($errors, 'No errors returned for example.com');
// Test our hook implementation, which denies fail.example.com explicitly.
$errors = $validator->validate('fail.example.com');
$this->assertNotEmpty($errors, 'Errors returned for fail.example.com');
$this->assertTrue(current($errors) == 'Fail.example.com cannot be registered', 'Error message returned correctly.');
}
/**
* Tests domain request alteration.
*/
public function testHookDomainRequestAlter() {
// Set the request.
$negotiator = \Drupal::service('domain.negotiator');
$negotiator->setRequestDomain($this->base_hostname);
// Check that the property was added by our hook.
$domain = $negotiator->getActiveDomain();
$this->assertTrue($domain->foo1 == 'bar1', 'The foo1 property was set to <em>bar1</em> by hook_domain_request_alter');
}
/**
* Tests domain operations hook.
*/
public function testHookDomainOperations() {
$domain = $this->domainStorage->load($this->key);
// Set the request.
$operations = $this->moduleHandler->invokeAll('domain_operations', array($domain, $this->currentUser));
// Test that our operations were added by the hook.
$this->assertTrue(isset($operations['domain_test']), 'Domain test operation loaded.');
}
/**
* Tests domain references alter hook.
*/
public function testHookDomainReferencesAlter() {
$domain = $this->domainStorage->load($this->key);
// Set the request.
$manager = \Drupal::service('entity.manager');
$target_type = 'domain';
// Build a node entity selection query.
$query = $manager->getStorage($target_type)->getQuery();
$context = [
'entity_type' => 'node',
'bundle' => 'article',
'field_type' => 'editor',
];
// Run the alteration, which should add metadata to the query for nodes.
$this->moduleHandler->alter('domain_references', $query, $this->currentUser, $context);
$this->assertTrue($query->getMetaData('domain_test') == 'Test string', 'Domain test query altered.');
// Build a user entity selection query.
$query = $manager->getStorage($target_type)->getQuery();
$context = [
'entity_type' => 'user',
'bundle' => 'user',
'field_type' => 'admin',
];
// Run the alteration, which does not add metadata for user queries.
$this->moduleHandler->alter('domain_references', $query, $this->currentUser, $context);
$this->assertEmpty($query->getMetaData('domain_test'), 'Domain test query not altered.');
}
}
@@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\domain\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\domain\Traits\DomainTestTrait;
/**
* Tests the ability to set a variable scheme on a domain.
*
* @group domain
*/
class DomainVariableSchemeTest extends KernelTestBase {
use DomainTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('domain');
/**
* Domain id key.
*/
public $key = 'example_com';
/**
* The Domain storage handler service.
*/
public $domainStorage;
/**
* Test setup.
*/
protected function setUp() {
parent::setUp();
// Create a domain.
$this->domainCreateTestDomains();
// Get the services.
$this->domainStorage = \Drupal::service('entity_type.manager')->getStorage('domain');
}
/**
* Tests domain loading.
*/
public function testDomainScheme() {
// Set our testing parameters.
$default_scheme = \Drupal::request()->getScheme();
$alt_scheme = ($default_scheme == 'https') ? 'http' : 'https';
$add_suffix = FALSE;
// Our created domain should have a scheme that matches the request.
$domain = $this->domainStorage->load($this->key);
$this->assertTrue($domain->getScheme($add_suffix) == $default_scheme);
// Swtich the scheme and see if that works.
$domain->set('scheme', $alt_scheme);
$domain->save();
$domain = $this->domainStorage->load($this->key);
$this->assertTrue($domain->getScheme($add_suffix) == $alt_scheme);
// Set the scheme to variable, and that should match the default.
$domain->set('scheme', 'variable');
$domain->save();
$this->assertTrue($domain->getScheme($add_suffix) == $default_scheme);
}
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\Tests\domain\Traits;
use Drupal\domain\DomainInterface;
/**
* Contains helper classes for tests to set up various configuration.
*/
trait DomainTestTrait {
/**
* Generates a list of domains for testing.
*
* In my environment, I use the example.com hostname as a base. Then I name
* hostnames one.* two.* up to ten. Note that we always use *_example_com
* for the machine_name (entity id) value, though the hostname can vary
* based on the system. This naming allows us to load test schema files.
*
* The script may also add test1, test2, test3 up to any number to test a
* large number of domains.
*
* @param int $count
* The number of domains to create.
* @param string|NULL $base_hostname
* The root domain to use for domain creation (e.g. example.com).
* @param array $list
* An optional list of subdomains to apply instead of the default set.
*/
public function domainCreateTestDomains($count = 1, $base_hostname = NULL, $list = array()) {
$original_domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
if (empty($base_hostname)) {
$base_hostname = $this->base_hostname;
}
// Note: these domains are rigged to work on my test server.
// For proper testing, yours should be set up similarly, but you can pass a
// $list array to change the default.
if (empty($list)) {
$list = array('', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten');
}
for ($i = 0; $i < $count; $i++) {
if ($i === 0) {
$hostname = $base_hostname;
$machine_name = 'example.com';
$name = 'Example';
}
elseif (!empty($list[$i])) {
$hostname = $list[$i] . '.' . $base_hostname;
$machine_name = $list[$i] . '.example.com';
$name = 'Test ' . ucfirst($list[$i]);
}
// These domains are not setup and are just for UX testing.
else {
$hostname = 'test' . $i . '.' . $base_hostname;
$machine_name = 'test' . $i . '.example.com';
$name = 'Test ' . $i;
}
// Create a new domain programmatically.
$values = array(
'hostname' => $hostname,
'name' => $name,
'id' => \Drupal::service('entity_type.manager')->getStorage('domain')->createMachineName($machine_name),
);
$domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values);
$domain->save();
}
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
}
/**
* Adds a test domain to an entity.
*
* @param string $entity_type
* The entity type being acted upon.
* @param int $entity_id
* The entity id.
* @param array $ids
* An array of ids to add.
* @param string $field
* The name of the domain field used to attach to the entity.
*/
public function addDomainsToEntity($entity_type, $entity_id, $ids, $field) {
if ($entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) {
$entity->set($field, $ids);
$entity->save();
}
}
/**
* Returns an uncached list of all domains.
*
* @return array
* An array of domain entities.
*/
public function getDomains() {
\Drupal::service('entity_type.manager')->getStorage('domain')->resetCache();
return \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple();
}
/**
* Returns an uncached list of all domains, sorted by weight.
*
* @return array
* An array of domain entities.
*/
public function getDomainsSorted() {
\Drupal::service('entity_type.manager')->getStorage('domain')->resetCache();
return \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultipleSorted();
}
/**
* Converts a domain hostname to a trusted host pattern.
*
* @param $hostname
* A hostname string.
*
* @return
* A regex-safe hostname, without delimiters.
*/
public function prepareTrustedHostname($hostname) {
$hostname = strtolower(preg_replace('/:\d+$/', '', trim($hostname)));
return preg_quote($hostname);
}
/**
* Set the base hostname for this test.
*/
public function setBaseHostname() {
$this->base_hostname = \Drupal::service('entity_type.manager')->getStorage('domain')->createHostname();
}
/**
* Reusable test function for checking initial / empty table status.
*/
public function domainTableIsEmpty() {
$domains = \Drupal::service('entity_type.manager')->getStorage('domain')->loadMultiple(NULL, TRUE);
$this->assertTrue(empty($domains), 'No domains have been created.');
$default_id = \Drupal::service('entity_type.manager')->getStorage('domain')->loadDefaultId();
$this->assertTrue(empty($default_id), 'No default domain has been set.');
}
/**
* Creates domain record for use with POST request tests.
*/
public function domainPostValues() {
$edit = array();
$domain = \Drupal::service('entity_type.manager')->getStorage('domain')->create();
$required = \Drupal::service('domain.validator')->getRequiredFields();
foreach ($required as $key) {
$edit[$key] = $domain->get($key);
}
// URL validation has issues on Travis, so only do it when requested.
$edit['validate_url'] = 0;
$edit['id'] = \Drupal::service('entity_type.manager')->getStorage('domain')->createMachineName($edit['hostname']);
return $edit;
}
}