updated contrib modules

This commit is contained in:
Bachir Soussi Chiadmi
2017-11-14 16:12:58 +01:00
parent c2b4e25be4
commit b32fe9ef14
317 changed files with 4672 additions and 13347 deletions
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,8 @@
{
"name": "drupal/config_devel",
"description": "Helps developers work with configuration.",
"type": "drupal-module",
"license": "GPL-2.0+",
"minimum-stability": "dev",
"require": { }
}
@@ -0,0 +1,2 @@
auto_import: []
auto_export: []
@@ -0,0 +1,23 @@
config_devel.settings:
type: config_object
label: config devel settings
mapping:
auto_import:
type: sequence
sequence:
- type: mapping
mapping:
filename:
type: string
hash:
type: string
auto_export:
type: sequence
sequence:
- type: string
config_devel.test:
type: config_object
label: config devel test
mapping:
label:
type: label
@@ -0,0 +1,13 @@
name: Configuration development module
type: module
description: 'Helps developers work with configuration.'
package: Configuration
# version: VERSION
# core: 8.x
configure: config_devel.settings
# Information added by Drupal.org packaging script on 2017-09-11
version: '8.x-1.1'
core: '8.x'
project: 'config_devel'
datestamp: 1505141047
@@ -0,0 +1,5 @@
config_devel.settings:
title: Config devel
description: 'Specify which configuration objects to automatically import/export.'
route_name: config_devel.settings
parent: system.admin_config_development
@@ -0,0 +1,7 @@
config_devel.settings:
path: '/admin/config/development/config_devel'
defaults:
_form: 'Drupal\config_devel\Form\ConfigDevelSettingsForm'
_title: 'Config Devel'
requirements:
_permission: 'import configuration'
@@ -0,0 +1,11 @@
services:
config_devel.writeback_subscriber:
class: Drupal\config_devel\EventSubscriber\ConfigDevelAutoExportSubscriber
arguments: ['@config.factory', '@config.manager', '@event_dispatcher']
tags:
- { name: event_subscriber }
config_devel.auto_import_subscriber:
class: Drupal\config_devel\EventSubscriber\ConfigDevelAutoImportSubscriber
arguments: ['@config.factory', '@config.manager']
tags:
- { name: event_subscriber }
@@ -0,0 +1,246 @@
<?php
/**
* @file
* Configuration development module drush integration.
*/
use Drupal\Core\Config\InstallStorage;
/**
* Implements hook_drush_command().
*/
function config_devel_drush_command() {
$items = array();
$description = '';
$description .= "List which configuration settings you want to export in the\n";
$description .= "module's info file by listing them under 'config_devel', as shown below:\n";
$description .= "\n";
$description .= "config_devel:\n";
$description .= " install:\n";
$description .= " - entity.view_display.node.article.default\n";
$description .= " - entity.view_display.node.article.teaser\n";
$description .= " - field.instance.node.article.body\n";
$description .= " optional:\n";
$description .= " - field.instance.node.article.tags\n";
$items['config-devel-export'] = array(
'description' => "Write back configuration to module's config directory.\n" . $description,
'arguments' => array(
'module' => 'Module machine name.',
),
'options' => array(
),
'required-arguments' => TRUE,
'examples' => array(
'drush config-devel-export MODULE_NAME' => 'Write back configuration to the specified module, based on .info file.',
),
'aliases' => array('cde', 'cd-em'),
);
$items['config-devel-import'] = array(
'description' => "Import configuration from module's config directory to active storage.\n" . $description,
'arguments' => array(
'module' => 'Module machine name.',
),
'options' => array(
),
'required-arguments' => TRUE,
'examples' => array(
'drush config-devel-import MODULE_NAME' => 'Import configuration from the specified module into the active storage, based on .info file.',
),
'aliases' => array('cdi', 'cd-im'),
);
$items['config-devel-import-one'] = array(
'description' => "Import a single configuration item from module's config directory to active storage.\n" . $description,
'arguments' => array(
'path' => 'Config file name.',
),
'options' => array(
),
'required-arguments' => TRUE,
'examples' => array(
'drush config-devel-import-one system.site.yml' => 'Import the contents of system.site.yml into the config object system.site.',
'drush config-devel-import-one system.site' => 'Import the standard input into the config object system.site. Helpful for scripting copying to remote',
),
'aliases' => array('cdi1', 'cd-i1'),
);
return $items;
}
/**
* Drush command callback.
*/
function drush_config_devel_export($extension) {
// Determine the type of extension we're dealing with.
$type = drush_config_devel_get_type($extension);
if ($type) {
// Get the config
$config = drush_config_devel_get_config($type, $extension);
// Process config
if (isset($config['install'])) {
drush_config_devel_process_config($config['install'], $type, $extension, InstallStorage::CONFIG_INSTALL_DIRECTORY);
}
// If we have any optional configuration, process that as well.
if (isset($config['optional'])) {
drush_config_devel_process_config($config['optional'], $type, $extension, InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
}
}
else {
drush_set_error("Couldn't export configuration. The '$extension' extension is not enabled.");
}
}
/**
* Drush command callback.
*/
function drush_config_devel_import($extension) {
// Determine the type of extension we're dealing with.
$type = drush_config_devel_get_type($extension);
if ($type) {
// Get the config
$config = drush_config_devel_get_config($type, $extension);
// Import config
if (isset($config['install'])) {
drush_config_devel_import_config($config['install'], $type, $extension, InstallStorage::CONFIG_INSTALL_DIRECTORY);
}
// Import optional config
if (isset($config['optional'])) {
drush_config_devel_import_config($config['optional'], $type, $extension, InstallStorage::CONFIG_OPTIONAL_DIRECTORY);
}
}
else {
drush_set_error("Couldn't import configuration. The '$extension' extension is not enabled.");
}
}
/**
* Drush command callback.
*/
function drush_config_devel_import_one($path) {
$contents = '';
if (!file_exists($path)) {
if (substr($path, -4) != '.yml') {
$contents = file_get_contents('php://stdin');
}
elseif (!empty($_SERVER['PWD'])) {
$path = $_SERVER['PWD'] . '/' . trim($path, '/');
}
}
if ($contents || file_exists($path)) {
\Drupal::service('config_devel.auto_import_subscriber')->importOne($path, '', $contents);
}
else {
drush_log("file '$path' not found", 'error');
exit;
}
}
/**
* Exports a list of configuration entities.
*
* @param array $config_list
* An array of configuration entities.
* @param string $type
* The type of extension we're exporting, one of module or theme.
* @param string $extension
* The module, theme or install profile we're exporting.
* @param string $directory
* The directory we're exporting to.
*/
function drush_config_devel_process_config($config_list, $type, $extension, $directory) {
$config_path = drupal_get_path($type, $extension) . "/$directory";
foreach ($config_list as $name) {
$config = \Drupal::config($name);
$file_names = array($config_path . '/' . $name . '.yml');
\Drupal::service('config_devel.writeback_subscriber')->writeBackConfig($config, $file_names);
}
}
/**
* Imports a list of configuration entities
*
* @param array $config_list
* An array of configuration entities.
* @param string $type
* The type of extension we're exporting, one of module or theme.
* @param string $extension
* The module, theme or install profile we're exporting.
* @param string $directory
* The directory we're exporting to.
*/
function drush_config_devel_import_config($config_list, $type, $extension, $directory) {
$config_path = drupal_get_path($type, $extension) . "/$directory";
foreach ($config_list as $name) {
$file_name = $config_path . '/' . $name . '.yml';
drush_config_devel_import_one($file_name);
}
}
/**
* Gets the config.
*
* @param string $type
* module, theme or profile
* @param string $extension
* extension name
* @return array
* An array containing install and optional config
*/
function drush_config_devel_get_config($type, $extension) {
$filename = drupal_get_path($type, $extension) . '/' . $extension .'.info.yml';
$info = \Drupal::service('info_parser')->parse($filename);
$config = array();
if (isset($info['config_devel'])) {
// Keep backwards compatibility for the old format. This has config names
// listed directly beneath 'config_devel', rather than an intermediate level
// for 'install' and 'optional'.
// Detect the old format based on whether there's neither of these two keys.
if (!isset($info['config_devel']['install']) && !isset($info['config_devel']['optional'])) {
$info['config_devel']['install'] = $info['config_devel'];
}
$config['install'] = $info['config_devel']['install'];
// If we have any optional configuration, fetch that as well.
if (isset($info['config_devel']['optional'])) {
$config['optional'] = $info['config_devel']['optional'];
}
}
return $config;
}
/**
* Gets the type for the given extension.
*
* @param string $extension
* extension name
* @return string
* module, theme, profile, or FALSE if no valid extension provided.
*/
function drush_config_devel_get_type($extension) {
$type = NULL;
if (\Drupal::moduleHandler()->moduleExists($extension)) {
$type = 'module';
}
elseif (\Drupal::service('theme_handler')->themeExists($extension)) {
$type = 'theme';
}
elseif (drupal_get_profile() === $extension) {
$type = 'profile';
}
return $type;
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\config_devel\Event;
/**
* Defines events for config devel.
*
* @see \Drupal\config_devel\Event\ConfigDevelSaveEvent
*/
final class ConfigDevelEvents {
/**
* Name of the event fired when saving a config entity to disk.
*
* This event allows other modules to impact the configuration that is being
* written to disk
*
* @Event
*
* @see \Drupal\config_devel\Event\ConfigDevelSaveEvent
*
* @var string
*/
const SAVE = 'config_devel.save';
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\config_devel\Event;
use Symfony\Component\EventDispatcher\Event;
/**
* The ConfigDevelSaveEvent class.
*/
class ConfigDevelSaveEvent extends Event {
/**
* An array of files names that will be written.
*
* @var array
*/
protected $fileNames = [];
/**
* An array representing the config object to be written.
*
* @var array
*/
protected $data;
/**
* ConfigDevelSaveEvent constructor.
*
* @param array $file_names
* The config file names.
* @param array $data
* The config data.
*/
public function __construct(array $file_names, array $data) {
$this->fileNames = $file_names;
$this->data = $data;
}
/**
* Gets the filenames that will be saved.
*
* @return array
* An array of file names.
*/
public function getFileNames() {
return $this->fileNames;
}
/**
* Sets the file names.
*
* @param array $file_names
* An array of file names to be written out
*/
public function setFileNames(array $file_names) {
$this->fileNames = $file_names;
}
/**
* Gets the data representing the config object.
*
* @return array
* An array of config data.
*/
public function getData() {
return $this->data;
}
/**
* Sets the data for the config object.
*
* @param array $data
* The config data.
*/
public function setData(array $data) {
$this->data = $data;
}
}
@@ -0,0 +1,136 @@
<?php
namespace Drupal\config_devel\EventSubscriber;
use Drupal\config_devel\Event\ConfigDevelEvents;
use Drupal\config_devel\Event\ConfigDevelSaveEvent;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Yaml\Exception\DumpException;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigRenameEvent;
use Drupal\Core\Config\ConfigEvents;
/**
* ConfigDevelAutoExportSubscriber subscriber for configuration CRUD events.
*/
class ConfigDevelAutoExportSubscriber extends ConfigDevelSubscriberBase implements EventSubscriberInterface {
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs the ConfigDevelAutoExportSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(ConfigFactoryInterface $config_factory, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) {
parent::__construct($config_factory, $config_manager);
$this->configFactory = $config_factory;
$this->configManager = $config_manager;
$this->eventDispatcher = $event_dispatcher;
}
/**
* The files to automatically export.
*
* @var array
*/
protected $autoExportFiles;
/**
* React to configuration ConfigEvent::SAVE events.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The event to process.
*/
public function onConfigSave(ConfigCrudEvent $event) {
$this->autoExportConfig($event->getConfig());
}
/**
* React to configuration ConfigEvent::RENAME events.
*
* @param \Drupal\Core\Config\ConfigRenameEvent $event
* The event to process.
*/
public function onConfigRename(ConfigRenameEvent $event) {
$this->autoExportConfig($event->getConfig());
}
/**
* Automatically export configuration.
*
* @param Config $config
* The config object.
*/
protected function autoExportConfig(Config $config) {
$config_name = $config->getName();
$file_names = array_filter($this->getSettings()->get('auto_export') ?: [], function ($file_name) use ($config_name) {
return basename($file_name, '.' . FileStorage::getFileExtension()) == $config_name;
});
$this->writeBackConfig($config, $file_names);
}
/**
* write configuration back to files.
*
* @param \Drupal\Core\Config\Config $config
* The config object.
* @param array $file_names
* The file names to which the configuration should be written.
*/
public function writeBackConfig(Config $config, array $file_names) {
if ($file_names) {
$data = $config->get();
$config_name = $config->getName();
unset($data['_core']);
if ($entity_type_id = $this->configManager->getEntityTypeIdByName($config_name)) {
unset($data['uuid']);
}
// Let everyone else have a change to update the exported data.
$event = new ConfigDevelSaveEvent($file_names, $data);
$this->eventDispatcher->dispatch(ConfigDevelEvents::SAVE, $event);
$data = $event->getData();
$file_names = $event->getFileNames();
foreach ($file_names as $file_name) {
try {
file_put_contents($file_name, (new InstallStorage())->encode($data));
}
catch (DumpException $e) {
// Do nothing. What could we do?
}
}
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = array('onConfigSave', 10);
$events[ConfigEvents::RENAME][] = array('onConfigRename', 10);
return $events;
}
}
@@ -0,0 +1,83 @@
<?php
namespace Drupal\config_devel\EventSubscriber;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\InstallStorage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
class ConfigDevelAutoImportSubscriber extends ConfigDevelSubscriberBase implements EventSubscriberInterface {
/**
* Reinstall changed config files.
*/
public function autoImportConfig() {
$config = $this->getSettings();
$changed = FALSE;
foreach ($config->get('auto_import') as $key => $file) {
if ($new_hash = $this->importOne($file['filename'], $file['hash'])) {
$config->set("auto_import.$key.hash", $new_hash);
$changed = TRUE;
}
}
if ($changed) {
$config->save();
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('autoImportConfig', 20);
return $events;
}
/**
* @param string $filename
* @param string $original_hash
* @return bool
*/
public function importOne($filename, $original_hash = '', $contents = '') {
$hash = '';
if (!$contents && (!$contents = @file_get_contents($filename))) {
return $hash;
}
$needs_import = TRUE;
if ($original_hash) {
$hash = Crypt::hashBase64($contents);
if ($hash == $original_hash) {
$needs_import = FALSE;
}
}
if ($needs_import) {
$data = (new InstallStorage())->decode($contents);
$config_name = basename($filename, '.yml');
$entity_type_id = $this->configManager->getEntityTypeIdByName($config_name);
if ($entity_type_id) {
$entity_storage = $this->getStorage($entity_type_id);
$entity_id = $this->getEntityId($entity_storage, $config_name);
$entity_type = $entity_storage->getEntityType();
$id_key = $entity_type->getKey('id');
$data[$id_key] = $entity_id;
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$entity = $entity_storage->create($data);
if ($existing_entity = $entity_storage->load($entity_id)) {
$entity
->set('uuid', $existing_entity->uuid())
->enforceIsNew(FALSE);
}
$entity_storage->save($entity);
}
else {
$this->configFactory->getEditable($config_name)->setData($data)->save();
}
}
return $hash;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Drupal\config_devel\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
class ConfigDevelSubscriberBase {
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The configuration manager.
*
* @var \Drupal\Core\Config\ConfigManagerInterface
*/
protected $configManager;
/**
* Constructs the ConfigDevelAutoExportSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
* The configuration manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, ConfigManagerInterface $config_manager) {
$this->configFactory = $config_factory;
$this->configManager = $config_manager;
}
/**
* @param string $entity_type_id
*
* @return \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected function getStorage($entity_type_id) {
return $this->configManager->getEntityManager()->getStorage($entity_type_id);
}
/**
* @param \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_storage
* @param string $config_name
*
* @return string
*/
protected function getEntityId(ConfigEntityStorageInterface $entity_storage, $config_name) {
// getIDFromConfigName adds a dot but getConfigPrefix has a dot already.
return $entity_storage::getIDFromConfigName($config_name, $entity_storage->getEntityType()->getConfigPrefix());
}
/**
* @return \Drupal\Core\Config\Config
*/
protected function getSettings() {
return $this->configFactory->getEditable('config_devel.settings');
}
}
@@ -0,0 +1,100 @@
<?php
namespace Drupal\config_devel\Form;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Settings form for config devel.
*/
class ConfigDevelSettingsForm extends ConfigFormBase {
/**
* Name of the config being edited.
*/
const CONFIGNAME = 'config_devel.settings';
/**
* @var array
*/
protected $keys;
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$default_value = '';
foreach ($this->config(static::CONFIGNAME)->get('auto_import') as $file) {
$default_value .= $file['filename'] . "\n";
}
$form['auto_import'] = array(
'#type' => 'textarea',
'#title' => $this->t('Auto import'),
'#default_value' => $default_value,
'#description' => $this->t('When these files change, they will be automatically imported at the beginning of the next request.') . '<br>' .
$this->t('Enter one item per line. Each item should consist of a path relative to the Drupal root and a filename in the same form as a config YML file.') . '<br>' .
$this->t('For example: "modules/mymodule/config/install/node.type.mytype.yml".'),
);
$form['auto_export'] = array(
'#type' => 'textarea',
'#title' => $this->t('Auto export'),
'#default_value' => implode("\n", $this->config(static::CONFIGNAME)->get('auto_export')),
'#description' => $this->t('Automatically export to the files specified when the corresponding config item is changed in the admin UI.') . '<br>' .
$this->t('Enter one item per line. Each item should consist of a path relative to the Drupal root and a filename in the same form as a config YML file.') . '<br>' .
$this->t('For example: "modules/mymodule/config/install/node.type.mytype.yml".'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
foreach (array('auto_import', 'auto_export') as $key) {
$form_state->setValue($key, array_filter(preg_split("/\r\n/", $form_state->getValues()[$key])));
}
foreach ($form_state->getValues()['auto_import'] as $file) {
$name = basename($file, '.' . FileStorage::getFileExtension());
if (in_array($name, array('system.site', 'core.extension', 'simpletest.settings'))) {
$form_state->setErrorByName('auto_import', $this->t('@name is not compatible with this module', array('@name' => $name)));
}
}
parent::validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$auto_import = array();
foreach ($form_state->getValues()['auto_import'] as $file) {
$auto_import[] = array(
'filename' => $file,
'hash' => '',
);
}
$this->config(static::CONFIGNAME)
->set('auto_import', $auto_import)
->set('auto_export', $form_state->getValues()['auto_export'])
->save();
parent::submitForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'config_devel_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['config_devel.settings'];
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\Tests\config_devel\Kernel;
/**
* Tests the automated importer for config entities.
*
* @group config_devel
*/
class ConfigDevelSubscriberEntityTest extends ConfigDevelSubscriberTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('config_test');
/**
* {@inheritdoc}
*/
const CONFIGNAME = 'config_test.dynamic.test';
/**
* {@inheritdoc}
*/
protected function doAssert(array $data, array $exported_data) {
$entity = entity_load('config_test', 'test', TRUE);
$this->assertIdentical($data['label'], $entity->get('label'));
$this->assertIdentical($exported_data['label'], $data['label']);
$this->assertIdentical($exported_data['id'], 'test');
$this->assertFalse(isset($exported_data['uuid']));
}
}
@@ -0,0 +1,25 @@
<?php
namespace Drupal\Tests\config_devel\Kernel;
/**
* Tests the automated importer for raw config objects.
*
* @group config_devel
*/
class ConfigDevelSubscriberRawTest extends ConfigDevelSubscriberTestBase {
/**
* {@inheritdoc}
*/
const CONFIGNAME = 'config_devel.test';
/**
* {@inheritdoc}
*/
protected function doAssert(array $data, array $exported_data) {
$this->assertIdentical($data, $this->storage->read(static::CONFIGNAME));
$this->assertIdentical($data, $exported_data);
}
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\Tests\config_devel\Kernel;
use Drupal\Component\Serialization\Yaml;
use Drupal\KernelTests\KernelTestBase;
abstract class ConfigDevelSubscriberTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('config_devel');
/**
* Name of the config object.
*/
const CONFIGNAME = '';
/**
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* Test the import subscriber.
*/
public function testSubscribers() {
// Without this the config exporter breaks.
\Drupal::service('config.installer')->installDefaultConfig('module', 'config_devel');
$filename = 'public://'. static::CONFIGNAME . '.yml';
drupal_mkdir('public://exported');
$exported_filename = 'public://exported/' . static::CONFIGNAME . '.yml';
\Drupal::configFactory()->getEditable('config_devel.settings')
->set('auto_import', array(array(
'filename' => $filename,
'hash' => '',
)))
->set('auto_export', array(
$exported_filename,
))
->save();
$this->storage = \Drupal::service('config.storage');
$this->assertFalse($this->storage->exists(static::CONFIGNAME));
$subscriber = \Drupal::service('config_devel.auto_import_subscriber');
for ($i = 2; $i; $i--) {
$data['label'] = $this->randomString();
file_put_contents($filename, Yaml::encode($data));
// The import fires an export too.
$subscriber->autoImportConfig();
$this->doAssert($data, Yaml::decode(file_get_contents($exported_filename)));
}
}
/**
* Assert that the config import succeeded.
*
* @param array $writen_data
* The config data as written.
* @param array $exported_data
* The config data exported.
*/
abstract protected function doAssert(array $data, array $exported_data);
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\config_devel\Unit;
use org\bovigo\vfs\vfsStream;
use Drupal\Component\Serialization\Yaml;
use Drupal\config_devel\EventSubscriber\ConfigDevelAutoExportSubscriber;
/**
* @coversDefaultClass \Drupal\config_devel\EventSubscriber\ConfigDevelAutoExportSubscriber
* @group config_devel
*/
class ConfigDevelAutoExportSubscriberTest extends ConfigDevelTestBase {
/**
* Test ConfigDevelAutoExportSubscriber::writeBackConfig().
*/
public function testWriteBackConfig() {
$config_data = array(
'id' => $this->randomMachineName(),
'langcode' => 'en',
'uuid' => '836769f4-6791-402d-9046-cc06e20be87f',
);
$config = $this->getMockBuilder('\Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config->expects($this->any())
->method('getName')
->will($this->returnValue($this->randomMachineName()));
$config->expects($this->any())
->method('get')
->will($this->returnValue($config_data));
$file_names = array(
vfsStream::url('public://' . $this->randomMachineName() . '.yml'),
vfsStream::url('public://' . $this->randomMachineName() . '.yml'),
);
$configDevelSubscriber = new ConfigDevelAutoExportSubscriber($this->configFactory, $this->configManager, $this->eventDispatcher);
$configDevelSubscriber->writeBackConfig($config, $file_names);
$data = $config_data;
unset($data['uuid']);
unset($data['_core']);
foreach ($file_names as $file_name) {
$this->assertEquals($data, Yaml::decode(file_get_contents($file_name)));
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\config_devel\Unit;
use org\bovigo\vfs\vfsStream;
use Drupal\Tests\UnitTestCase;
/**
* Helper class with mock objects.
*/
abstract class ConfigDevelTestBase extends UnitTestCase {
/**
* @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $configFactory;
/**
* @var \Drupal\Core\Config\ConfigManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $configManager;
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
$this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface');
$this->configManager->expects($this->any())
->method('getEntityTypeIdByName')
->will($this->returnArgument(0));
vfsStream::setup('public://');
}
}
@@ -7,8 +7,8 @@ configure: devel.admin_settings
tags:
- developer
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -6,8 +6,8 @@ package: Development
tags:
- developer
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -141,6 +141,7 @@ class DevelGenerateCommands extends DrushCommands {
* @param $max_width Max width of first level of links.
* @option kill Delete all content before generating new content.
* @aliases genm
* @validate-module-enabled menu_link_content
*/
public function menus($number_menus = 2, $number_links = 50, $max_depth = 3, $max_width = 8, $options = ['kill' => FALSE]) {
$this->generate();
@@ -179,7 +180,7 @@ class DevelGenerateCommands extends DrushCommands {
/** @var DevelGenerateBaseInterface $instance */
$instance = $manager->createInstance($commandData->annotationData()->get('pluginId'), array());
$this->setPluginInstance($instance);
$parameters = $instance->validateDrushParams($args);
$parameters = $instance->validateDrushParams($args, $commandData->input()->getOptions());
$this->setParameters($parameters);
}
@@ -165,4 +165,8 @@ abstract class DevelGenerateBase extends PluginBase implements DevelGenerateBase
}
return $this->random;
}
protected function isDrush8() {
return function_exists('drush_drupal_load_autoloader');
}
}
@@ -316,7 +316,7 @@ class ContentDevelGenerate extends DevelGenerateBase implements ContainerFactory
$start = time();
for ($i = 1; $i <= $values['num']; $i++) {
$this->develGenerateContentAddNode($values);
if (function_exists('drush_log') && $i % drush_get_option('feedback', 1000) == 0) {
if ($this->isDrush8() && function_exists('drush_log') && $i % drush_get_option('feedback', 1000) == 0) {
$now = time();
drush_log(dt('Completed @feedback nodes (@rate nodes/min)', array('@feedback' => drush_get_option('feedback', 1000), '@rate' => (drush_get_option('feedback', 1000) * 60) / ($now - $start))), 'ok');
$start = $now;
@@ -372,8 +372,8 @@ class ContentDevelGenerate extends DevelGenerateBase implements ContainerFactory
/**
* {@inheritdoc}
*/
public function validateDrushParams($args) {
$add_language = drush_get_option('languages');
public function validateDrushParams($args, $options = []) {
$add_language = $this->isDrush8() ? drush_get_option('languages') : $options['languages'];
if (!empty($add_language)) {
$add_language = explode(',', str_replace(' ', '', $add_language));
// Intersect with the enabled languages to make sure the language args
@@ -381,17 +381,17 @@ class ContentDevelGenerate extends DevelGenerateBase implements ContainerFactory
$values['values']['add_language'] = array_intersect($add_language, array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL)));
}
$values['kill'] = drush_get_option('kill');
$values['kill'] = $this->isDrush8() ? drush_get_option('kill') : $options['kill'];
$values['title_length'] = 6;
$values['num'] = array_shift($args);
$values['max_comments'] = array_shift($args);
$all_types = array_keys(node_type_get_names());
$default_types = array_intersect(array('page', 'article'), $all_types);
$selected_types = StringUtils::csvToArray(drush_get_option('types', $default_types));
// Validates the input format for content types option.
if (drush_get_option('types', $default_types) === TRUE) {
throw new \Exception(dt('Wrong syntax or no content type selected. The correct syntax uses "=", eg.: --types=page,article'));
if ($this->isDrush8()) {
$selected_types = _convert_csv_to_array(drush_get_option('types', $default_types));
}
else {
$selected_types = StringUtils::csvToArray($options['types'] ?: $default_types);
}
if (empty($selected_types)) {
@@ -221,14 +221,14 @@ class MenuDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
/**
* {@inheritdoc}
*/
public function validateDrushParams($args) {
public function validateDrushParams($args, $options = []) {
$link_types = array('node', 'front', 'external');
$values = array(
'num_menus' => array_shift($args),
'num_links' => array_shift($args),
'kill' => drush_get_option('kill'),
'pipe' => drush_get_option('pipe'),
'kill' => $this->isDrush8() ? drush_get_option('kill') : $options['kill'],
'pipe' => $this->isDrush8() ? drush_get_option('pipe') : $options['pipe'],
'link_types' => array_combine($link_types, $link_types),
);
@@ -240,16 +240,16 @@ class MenuDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
$values['existing_menus']['__new-menu__'] = TRUE;
if ($this->isNumber($values['num_menus']) == FALSE) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid number of menus'));
throw new \Exception(dt('Invalid number of menus'));
}
if ($this->isNumber($values['num_links']) == FALSE) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid number of links'));
throw new \Exception(dt('Invalid number of links'));
}
if ($this->isNumber($values['max_depth']) == FALSE || $values['max_depth'] > 9 || $values['max_depth'] < 1) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid maximum link depth. Use a value between 1 and 9'));
throw new \Exception(dt('Invalid maximum link depth. Use a value between 1 and 9'));
}
if ($this->isNumber($values['max_width']) == FALSE || $values['max_width'] < 1) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid maximum menu width. Use a positive numeric value.'));
throw new \Exception(dt('Invalid maximum menu width. Use a positive numeric value.'));
}
return $values;
@@ -300,19 +300,17 @@ class MenuDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
protected function generateMenus($num_menus, $title_length = 12) {
$menus = array();
if ($this->moduleHandler->moduleExists('menu_ui')) {
for ($i = 1; $i <= $num_menus; $i++) {
$name = $this->getRandom()->word(mt_rand(2, max(2, $title_length)));
for ($i = 1; $i <= $num_menus; $i++) {
$name = $this->getRandom()->word(mt_rand(2, max(2, $title_length)));
$menu = $this->menuStorage->create(array(
'label' => $name,
'id' => 'devel-' . Unicode::strtolower($name),
'description' => $this->t('Description of @name', array('@name' => $name)),
));
$menu = $this->menuStorage->create(array(
'label' => $name,
'id' => 'devel-' . Unicode::strtolower($name),
'description' => $this->t('Description of @name', array('@name' => $name)),
));
$menu->save();
$menus[$menu->id()] = $menu->label();
}
$menu->save();
$menus[$menu->id()] = $menu->label();
}
return $menus;
@@ -210,15 +210,6 @@ class TermDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
$max++;
if (function_exists('drush_log')) {
$feedback = drush_get_option('feedback', 1000);
if ($i % $feedback == 0) {
$now = time();
drush_log(dt('Completed @feedback terms (@rate terms/min)', array('@feedback' => $feedback, '@rate' => $feedback * 60 / ($now - $start))), 'ok');
$start = $now;
}
}
// Limit memory usage. Only report first 20 created terms.
if ($i < 20) {
$terms[] = $term->label();
@@ -233,7 +224,7 @@ class TermDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
/**
* {@inheritdoc}
*/
public function validateDrushParams($args) {
public function validateDrushParams($args, $options = []) {
$vocabulary_name = array_shift($args);
$number = array_shift($args);
@@ -242,21 +233,21 @@ class TermDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
}
if (!$vocabulary_name) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Please provide a vocabulary machine name.'));
throw new \Exception(dt('Please provide a vocabulary machine name.'));
}
if (!$this->isNumber($number)) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid number of terms: @num', array('@num' => $number)));
throw new \Exception(dt('Invalid number of terms: @num', array('@num' => $number)));
}
// Try to convert machine name to a vocabulary id.
if (!$vocabulary = $this->vocabularyStorage->load($vocabulary_name)) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid vocabulary name: @name', array('@name' => $vocabulary_name)));
throw new \Exception(dt('Invalid vocabulary name: @name', array('@name' => $vocabulary_name)));
}
$values = [
'num' => $number,
'kill' => drush_get_option('kill'),
'kill' => $this->isDrush8() ? drush_get_option('kill') : $options['kill'],
'title_length' => 12,
'vids' => [$vocabulary->id()],
];
@@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\devel_generate\DevelGenerateBase;
use Drush\Utils\StringUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -177,14 +178,26 @@ class UserDevelGenerate extends DevelGenerateBase implements ContainerFactoryPlu
/**
* {@inheritdoc}
*/
public function validateDrushParams($args) {
public function validateDrushParams($args, $options = []) {
$values = array(
'num' => array_shift($args),
'roles' => drush_get_option('roles') ? explode(',', drush_get_option('roles')) : array(),
'kill' => drush_get_option('kill'),
'pass' => drush_get_option('pass', NULL),
'time_range' => 0,
);
if ($this->isDrush8()) {
$values += [
'roles' => explode(',', drush_get_option('roles', '')),
'kill' => drush_get_option('kill'),
'pass' => drush_get_option('pass', NULL),
];
}
else {
$values += [
'roles' => StringUtils::csvToArray($options['roles']),
'kill' => $options['kill'],
'pass' => $options['pass'],
];
}
return $values;
}
@@ -158,15 +158,15 @@ class VocabularyDevelGenerate extends DevelGenerateBase implements ContainerFact
/**
* {@inheritdoc}
*/
public function validateDrushParams($args) {
public function validateDrushParams($args, $options = []) {
$values = array(
'num' => array_shift($args),
'kill' => drush_get_option('kill'),
'kill' => $this->isDrush8() ? drush_get_option('kill') : $options['kill'],
'title_length' => 12,
);
if ($this->isNumber($values['num']) == FALSE) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', dt('Invalid number of vocabularies: @num.', array('@num' => $values['num'])));
throw new \Exception(dt('Invalid number of vocabularies: @num.', array('@num' => $values['num'])));
}
return $values;
@@ -7,8 +7,8 @@ configure: admin/config/development/generate
tags:
- developer
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -1,6 +1,6 @@
services:
devel.command:
class: Drupal\devel\Commands\DevelCommands
arguments: ['@token', '@service_container', '@event_dispatcher']
arguments: ['@token', '@service_container', '@event_dispatcher', '@module_handler']
tags:
- { name: drush.command }
@@ -6,8 +6,8 @@ package: Development
tags:
- developer
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -5,6 +5,9 @@ use Drupal\Component\Uuid\Php;
use Drupal\Core\Utility\Token;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\UserAbortException;
use Drush\Utils\StringUtils;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Output\Output;
/**
* For commands that are parts of modules, Drush expects to find commandfiles in
@@ -21,11 +24,21 @@ class DevelCommands extends DrushCommands {
protected $eventDispatcher;
public function __construct(Token $token, $container, $eventDispatcher) {
protected $moduleHandler;
public function __construct(Token $token, $container, $eventDispatcher, $moduleHandler) {
parent::__construct();
$this->token = $token;
$this->container = $container;
$this->eventDispatcher = $eventDispatcher;
$this->moduleHandler = $moduleHandler;
}
/**
* @return \Drupal\Core\Extension\ModuleHandlerInterface
*/
public function getModuleHandler() {
return $this->moduleHandler;
}
/**
@@ -50,93 +63,110 @@ class DevelCommands extends DrushCommands {
}
/**
* Uninstall, and Install a list of modules.
* Uninstall, and Install modules.
* @command devel-reinstall
* @command devel:reinstall
* @param $modules A comma-separated list of module names.
* @aliases dre
* @aliases dre,devel-reinstall
* @allow-additional-options pm-uninstall,pm-enable
*/
public function reinstall($projects) {
$projects = _convert_csv_to_array($projects);
public function reinstall($modules) {
$modules = StringUtils::csvToArray($modules);
// This is faster than 3 separate bootstraps.
$args = array_merge(array('pm-uninstall'), $projects);
// @todo. Use $application dispatch instead of drush_invoke().
call_user_func_array('drush_invoke', $args);
$args = array_merge(array('pm-enable'), $projects);
call_user_func_array('drush_invoke', $args);
$modules_str = implode(',', $modules);
drush_invoke_process('@self', 'pm:uninstall', [$modules_str], []);
drush_invoke_process('@self', 'pm:enable', [$modules_str], []);
}
/**
* List implementations of a given hook and optionally edit one.
*
* @command devel-hook
* @command devel:hook
* @param $hook The name of the hook to explore.
* @param $implementation The name of the implementation to edit. Usually omitted.
* @usage devel-hook cron
* List implementations of hook_cron().
* @aliases fnh,fn-hook,hook
* @aliases fnh,fn-hook,hook,devel-hook
* @optionset_get_editor
*/
function hook($hook) {
function hook($hook, $implementation) {
// Get implementations in the .install files as well.
include_once './core/includes/install.inc';
drupal_load_updates();
$info = $this->codeLocate($implementation . "_$hook");
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
}
if ($hook_implementations = \Drupal::moduleHandler()->getImplementations($hook)) {
if ($choice = drush_choice(array_combine($hook_implementations, $hook_implementations), 'Enter the number of the hook implementation you wish to view.')) {
$info= $this->codeLocate($choice . "_$hook");
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
/**
* @hook interact hook
*/
public function hookInteract(Input $input, Output $output) {
if (!$input->getArgument('implementation')) {
if ($hook_implementations = $this->getModuleHandler()->getImplementations($input->getArgument('hook'))) {
if (!$choice = $this->io()->choice('Enter the number of the hook implementation you wish to view.', array_combine($hook_implementations, $hook_implementations))) {
throw new UserAbortException();
}
$input->setArgument('implementation', $choice);
}
else {
throw new \Exception(dt('No implementations'));
}
}
else {
$this->logger()->success(dt('No implementations.'));
}
}
/**
* List implementations of a given event and optionally edit one.
*
* @command devel-event
* @command devel:event
* @param $event The name of the event to explore. If omitted, a list of events is shown.
* @param $implementation The name of the implementation to show. Usually omitted.
* @usage devel-event
* Pick a Kernel event, then pick an implementation, and then view its source code.
* @usage devel-event kernel.terminate
* Pick a terminate subscribers and view its source code.
* Pick a terminate subscribers implementation and view its source code.
* @aliases fne,fn-event,event
*/
function event($event) {
function event($event, $implementation) {
$info= $this->codeLocate($implementation);
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
}
/**
* @hook interact devel:event
*/
public function interactEvent(Input $input, Output $output) {
$dispatcher = $this->getEventDispatcher();
if (empty($event)) {
// @todo Expand this list and move to interact().
if (!$input->getArgument('event')) {
// @todo Expand this list.
$events = array('kernel.controller', 'kernel.exception', 'kernel.request', 'kernel.response', 'kernel.terminate', 'kernel.view');
$events = array_combine($events, $events);
if (!$event = drush_choice($events, 'Enter the event you wish to explore.')) {
if (!$event = $this->io()->choice('Enter the event you wish to explore.', $events)) {
throw new UserAbortException();
}
$input->setArgument('event', $event);
}
if ($implementations = $dispatcher->getListeners($event)) {
foreach ($implementations as $implementation) {
$callable = get_class($implementation[0]) . '::' . $implementation[1];
$choices[$callable] = $callable;
}
if ($choice = drush_choice($choices, 'Enter the number of the implementation you wish to view.')) {
$info= $this->codeLocate($choice);
$exec = drush_get_editor();
drush_shell_exec_interactive($exec, $info['file']);
if (!$choice = $this->io()->choice('Enter the number of the implementation you wish to view.', $choices)) {
throw new UserAbortException();
}
$input->setArgument('implementation', $choice);
}
else {
$this->logger()->success(dt('No implementations.'));
throw new \Exception(dt('No implementations.'));
}
}
/**
* List available tokens.
*
* @command devel-token
* @aliases token
* @command devel:token
* @aliases token,devel-token
* @field-labels
* group: Group
* token: Token
@@ -162,8 +192,8 @@ class DevelCommands extends DrushCommands {
/**
* Generate a UUID.
*
* @command devel-uuid
* @aliases uuid
* @command devel:uuid
* @aliases uuid,devel-uuid
* @usage drush devel-uuid
* Outputs a Universally Unique Identifier.
*
@@ -203,9 +233,9 @@ class DevelCommands extends DrushCommands {
/**
* Get a list of available container services.
*
* @command devel-services
* @command devel:services
* @param $prefix A prefix to filter the service list by.
* @aliases devel-container-services,dcs
* @aliases devel-container-services,dcs,devel-services
* @usage drush devel-services
* Gets a list of all available container services
* @usage drush dcs plugin.manager
@@ -5,8 +5,8 @@ package: Testing
# version: VERSION
# core: 8.x
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -9,8 +9,8 @@ dependencies:
- text
- entity_test
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -5,8 +5,8 @@ package: Testing
# version: VERSION
# core: 8.x
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -1,22 +1,22 @@
<?php
namespace Drupal\devel\Tests;
namespace Drupal\Tests\devel\Functional;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Devel controller.
*
* @group devel
*/
class DevelControllerTest extends WebTestBase {
class DevelControllerTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('devel', 'node', 'entity_test', 'devel_entity_test', 'block');
public static $modules = ['devel', 'node', 'entity_test', 'devel_entity_test', 'block'];
/**
* {@inheritdoc}
@@ -26,35 +26,35 @@ class DevelControllerTest extends WebTestBase {
// Create a test entity.
$random_label = $this->randomMachineName();
$data = array('type' => 'entity_test', 'name' => $random_label);
$data = ['type' => 'entity_test', 'name' => $random_label];
$this->entity = entity_create('entity_test', $data);
$this->entity->save();
// Create a test entity with only canonical route.
$random_label = $this->randomMachineName();
$data = array('type' => 'devel_entity_test_canonical', 'name' => $random_label);
$data = ['type' => 'devel_entity_test_canonical', 'name' => $random_label];
$this->entity_canonical = entity_create('devel_entity_test_canonical', $data);
$this->entity_canonical->save();
// Create a test entity with only edit route.
$random_label = $this->randomMachineName();
$data = array('type' => 'devel_entity_test_edit', 'name' => $random_label);
$data = ['type' => 'devel_entity_test_edit', 'name' => $random_label];
$this->entity_edit = entity_create('devel_entity_test_edit', $data);
$this->entity_edit->save();
// Create a test entity with no routes.
$random_label = $this->randomMachineName();
$data = array('type' => 'devel_entity_test_no_links', 'name' => $random_label);
$data = ['type' => 'devel_entity_test_no_links', 'name' => $random_label];
$this->entity_no_links = entity_create('devel_entity_test_no_links', $data);
$this->entity_no_links->save();
$this->drupalPlaceBlock('local_tasks_block');
$web_user = $this->drupalCreateUser(array(
$web_user = $this->drupalCreateUser([
'view test entity',
'administer entity_test content',
'access devel information',
));
]);
$this->drupalLogin($web_user);
}
@@ -1,16 +1,16 @@
<?php
namespace Drupal\devel\Tests;
namespace Drupal\Tests\devel\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests pluggable dumper feature.
*
* @group devel
*/
class DevelDumperTest extends WebTestBase {
class DevelDumperTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -36,36 +36,42 @@ class DevelDumperTest extends WebTestBase {
$this->drupalGet('admin/config/development/devel');
// Ensures that the dumper input is present on the config page.
$this->assertFieldByName('dumper');
$this->assertSession()->fieldExists('dumper');
// Ensures that the 'default' dumper is enabled by default.
$this->assertFieldChecked('edit-dumper-default');
$this->assertSession()->checkboxChecked('edit-dumper-default');
// Ensures that all dumpers declared by devel are present on the config page
// and that only the available dumpers are selectable.
$dumpers = ['default', 'drupal_variable', 'firephp', 'chromephp', 'var_dumper'];
$dumpers = [
'default',
'drupal_variable',
'firephp',
'chromephp',
'var_dumper',
];
$available_dumpers = ['default', 'drupal_variable'];
foreach ($dumpers as $dumper) {
$this->assertFieldByXPath('//input[@type="radio" and @name="dumper"]', $dumper, new FormattableMarkup('Radio button for @dumper found.', ['@dumper' => $dumper]));
$this->assertFieldByXPath('//input[@type="radio" and @name="dumper"]', $dumper);
if (in_array($dumper, $available_dumpers)) {
$this->assertFieldByXPath('//input[@name="dumper" and not(@disabled="disabled")]', $dumper, new FormattableMarkup('Dumper @dumper is available.', ['@dumper' => $dumper]));
$this->assertFieldByXPath('//input[@name="dumper" and not(@disabled="disabled")]', $dumper);
}
else {
$this->assertFieldByXPath('//input[@name="dumper" and @disabled="disabled"]', $dumper, new FormattableMarkup('Dumper @dumper is disabled.', ['@dumper' => $dumper]));
$this->assertFieldByXPath('//input[@name="dumper" and @disabled="disabled"]', $dumper);
}
}
// Ensures that dumper plugins declared by other modules are present on the
// config page and that only the available dumpers are selectable.
$this->assertFieldByName('dumper', 'available_test_dumper');
$this->assertText('Available test dumper.', 'Available dumper label is present');
$this->assertText('Drupal dumper for testing purposes (available).', 'Available dumper description is present');
$this->assertFieldByXPath('//input[@name="dumper"]', 'available_test_dumper');
$this->assertSession()->pageTextContains('Available test dumper.');
$this->assertSession()->pageTextContains('Drupal dumper for testing purposes (available).');
$this->assertFieldByXPath('//input[@name="dumper" and not(@disabled="disabled")]', 'available_test_dumper', 'Available dumper input not is disabled.');
$this->assertFieldByName('dumper', 'not_available_test_dumper');
$this->assertText('Not available test dumper.', 'Non available dumper label is present');
$this->assertText('Drupal dumper for testing purposes (not available).Not available. You may need to install external dependencies for use this plugin.', 'Non available dumper description is present');
$this->assertFieldByXPath('//input[@name="dumper"]', 'not_available_test_dumper');
$this->assertSession()->pageTextContains('Not available test dumper.');
$this->assertSession()->pageTextContains('Drupal dumper for testing purposes (not available).Not available. You may need to install external dependencies for use this plugin.');
$this->assertFieldByXPath('//input[@name="dumper" and @disabled="disabled"]', 'not_available_test_dumper', 'Non available dumper input is disabled.');
// Ensures that saving of the dumpers configuration works as expected.
@@ -73,10 +79,10 @@ class DevelDumperTest extends WebTestBase {
'dumper' => 'drupal_variable',
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->assertSession()->pageTextContains(t('The configuration options have been saved.'));
$config = \Drupal::config('devel.settings')->get('devel_dumper');
$this->assertEqual('drupal_variable', $config, 'The configuration options have been properly saved');
$this->assertEquals('drupal_variable', $config, 'The configuration options have been properly saved');
// Ensure that if the chosen dumper is not available (e.g. the module that
// provide it is uninstalled) the 'default' dumper appears selected in the
@@ -84,22 +90,22 @@ class DevelDumperTest extends WebTestBase {
\Drupal::service('module_installer')->install(['kint']);
$this->drupalGet('admin/config/development/devel');
$this->assertFieldByName('dumper', 'kint');
$this->assertFieldByXPath('//input[@name="dumper"]', 'kint');
$edit = [
'dumper' => 'kint',
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->assertSession()->pageTextContains(t('The configuration options have been saved.'));
$config = \Drupal::config('devel.settings')->get('devel_dumper');
$this->assertEqual('kint', $config, 'The configuration options have been properly saved');
$this->assertEquals('kint', $config, 'The configuration options have been properly saved');
\Drupal::service('module_installer')->uninstall(['kint']);
$this->drupalGet('admin/config/development/devel');
$this->assertNoFieldByName('dumper', 'kint');
$this->assertFieldChecked('edit-dumper-default');
$this->assertNoFieldByXPath('//input[@name="dumper"]', 'kint');
$this->assertSession()->checkboxChecked('edit-dumper-default');
}
/**
@@ -110,7 +116,7 @@ class DevelDumperTest extends WebTestBase {
'dumper' => 'available_test_dumper',
];
$this->drupalPostForm('admin/config/development/devel', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->assertSession()->pageTextContains(t('The configuration options have been saved.'));
$this->drupalGet('devel_dumper_test/dump');
$elements = $this->xpath('//body/pre[contains(text(), :message)]', [':message' => 'AvailableTestDumper::dump() Test output']);
@@ -129,8 +135,8 @@ class DevelDumperTest extends WebTestBase {
$this->assertTrue(!empty($elements), 'Dumped message is present.');
// Ensures that plugins can add libraries to the page when the
// ::exportAsRenderable() method is used.
$this->assertRaw('devel_dumper_test/css/devel_dumper_test.css');
$this->assertRaw('devel_dumper_test/js/devel_dumper_test.js');
$this->assertSession()->responseContains('devel_dumper_test/css/devel_dumper_test.css');
$this->assertSession()->responseContains('devel_dumper_test/js/devel_dumper_test.js');
$debug_filename = file_directory_temp() . '/drupal_debug.txt';
@@ -140,7 +146,7 @@ class DevelDumperTest extends WebTestBase {
<pre>AvailableTestDumper::export() Test output</pre>
EOF;
$this->assertEqual($file_content, $expected, 'Dumped message is present.');
$this->assertEquals($file_content, $expected, 'Dumped message is present.');
// Ensures that the DevelDumperManager::debug() is not access checked and
// that the dump is written in the debug file even if the user has not the
@@ -153,7 +159,7 @@ EOF;
<pre>AvailableTestDumper::export() Test output</pre>
EOF;
$this->assertEqual($file_content, $expected, 'Dumped message is present.');
$this->assertEquals($file_content, $expected, 'Dumped message is present.');
}
}
@@ -1,16 +1,16 @@
<?php
namespace Drupal\devel\Tests;
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests devel menu links.
*
* @group devel
*/
class DevelMenuLinksTest extends WebTestBase {
class DevelMenuLinksTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -1,22 +1,22 @@
<?php
namespace Drupal\devel\Tests;
namespace Drupal\Tests\devel\Functional;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests reinstall modules.
*
* @group devel
*/
class DevelReinstallTest extends WebTestBase {
class DevelModulesReinstallTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('devel');
public static $modules = ['devel'];
/**
* The profile to install as a basis for testing.
@@ -31,7 +31,7 @@ class DevelReinstallTest extends WebTestBase {
protected function setUp() {
parent::setUp();
$web_user = $this->drupalCreateUser(array('administer site configuration'));
$web_user = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($web_user);
}
@@ -40,7 +40,7 @@ class DevelReinstallTest extends WebTestBase {
*/
public function testDevelReinstallModules() {
// Minimal profile enables only dblog, block and node.
$modules = array('dblog', 'block');
$modules = ['dblog', 'block'];
// Needed for compare correctly the message.
sort($modules);
@@ -48,13 +48,13 @@ class DevelReinstallTest extends WebTestBase {
$this->drupalGet('devel/reinstall');
// Prepare field data in an associative array
$edit = array();
$edit = [];
foreach ($modules as $module) {
$edit["reinstall[$module]"] = TRUE;
}
$this->drupalPostForm('devel/reinstall', $edit, t('Reinstall'));
$this->assertText(t('Uninstalled and installed: @names.', array('@names' => implode(', ', $modules))));
$this->assertText(t('Uninstalled and installed: @names.', ['@names' => implode(', ', $modules)]));
}
}
@@ -1,16 +1,16 @@
<?php
namespace Drupal\devel\Tests;
namespace Drupal\Tests\devel\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\simpletest\WebTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests switch user.
*
* @group devel
*/
class DevelSwitchUserTest extends WebTestBase {
class DevelSwitchUserTest extends BrowserTestBase {
/**
* Modules to enable.
@@ -276,12 +276,12 @@ class DevelSwitchUserTest extends WebTestBase {
*/
protected function assertSessionByUid($uid) {
$query = \Drupal::database()->select('sessions');
$query->fields('sessions', array('uid'));
$query->fields('sessions', ['uid']);
$query->condition('uid', $uid);
$result = $query->execute()->fetchAll();
if (empty($result)) {
$this->fail(new FormattableMarkup('No session found for uid @uid', array('@uid' => $uid)));
$this->fail(new FormattableMarkup('No session found for uid @uid', ['@uid' => $uid]));
}
elseif (count($result) > 1) {
// If there is more than one session, then that must be unexpected.
@@ -304,7 +304,7 @@ class DevelSwitchUserTest extends WebTestBase {
*/
protected function assertNoSessionByUid($uid) {
$query = \Drupal::database()->select('sessions');
$query->fields('sessions', array('uid'));
$query->fields('sessions', ['uid']);
$query->condition('uid', $uid);
$result = $query->execute()->fetchAll();
$this->assert(empty($result), "No session for uid $uid found.");
@@ -56,7 +56,7 @@ class DatabaseDataCollector extends DataCollector implements DrupalDataCollector
unset($query['caller']['args']);
// Remove query args element if empty.
if (empty($query['args'])) {
if (isset($query['args']) && empty($query['args'])) {
unset($query['args']);
}
@@ -223,8 +223,11 @@ class DatabaseDataCollector extends DataCollector implements DrupalDataCollector
$query['type'] = $type;
$quoted = [];
foreach ((array) $query['args'] as $key => $val) {
$quoted[$key] = is_null($val) ? 'NULL' : $conn->quote($val);
if (isset($query['args'])) {
foreach ((array) $query['args'] as $key => $val) {
$quoted[$key] = is_null($val) ? 'NULL' : $conn->quote($val);
}
}
$query['query_args'] = strtr($query['query'], $quoted);
@@ -108,6 +108,13 @@ class ConfigEntityStorageDecorator extends EntityDecorator implements ConfigEnti
return $this->getOriginalObject()->save($entity);
}
/**
* {@inheritdoc}
*/
public function hasData() {
return $this->getOriginalObject()->hasData();
}
/**
* {@inheritdoc}
*/
@@ -4,6 +4,7 @@ namespace Drupal\webprofiler\EventDispatcher;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\webprofiler\Stopwatch;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpKernel\KernelEvents;
@@ -29,10 +30,19 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
protected $notCalledListeners;
/**
* @param \Drupal\webprofiler\Stopwatch $stopwatch
* {@inheritdoc}
*/
public function setStopwatch(Stopwatch $stopwatch) {
$this->stopwatch = $stopwatch;
public function __construct(ContainerInterface $container, array $listeners = []) {
parent::__construct($container, $listeners);
$this->notCalledListeners = $listeners;
}
/**
* {@inheritdoc}
*/
public function addListener($event_name, $listener, $priority = 0) {
parent::addListener($event_name, $listener, $priority);
$this->notCalledListeners[$event_name][$priority][] = ['callable' => $listener];
}
/**
@@ -97,6 +107,13 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
return $this->notCalledListeners;
}
/**
* @param \Drupal\webprofiler\Stopwatch $stopwatch
*/
public function setStopwatch(Stopwatch $stopwatch) {
$this->stopwatch = $stopwatch;
}
/**
* Called before dispatching the event.
*
@@ -158,38 +175,19 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
'method' => $definition['callable'][1],
];
// Remove this listener from the $notCalledListeners array.
if (!$this->notCalledListeners) {
$this->notCalledListeners = $this->cloneListeners($this->listeners);
}
foreach ($this->notCalledListeners[$event_name][$priority] as $key => $listener) {
if ($listener['service'][0] == $definition['service'][0] && $listener['service'][1] == $definition['service'][1]) {
unset($this->notCalledListeners[$event_name][$priority][$key]);
}
}
}
/**
* @param $listeners
*
* @return array
*/
private function cloneListeners($listeners) {
$clone = [];
foreach ($listeners as $eventName => $events) {
foreach ($events as $priorityValue => $priorities) {
foreach ($priorities as $key => $listener) {
$clone[$eventName][$priorityValue][$key]['service'] = [
$listener['service'][0],
$listener['service'][1]
];
if (isset($listener['service'])) {
if ($listener['service'][0] == $definition['service'][0] && $listener['service'][1] == $definition['service'][1]) {
unset($this->notCalledListeners[$event_name][$priority][$key]);
}
}
else {
if (get_class($listener['callable'][0]) == get_class($definition['callable'][0]) && $listener['callable'][1] == $definition['callable'][1]) {
unset($this->notCalledListeners[$event_name][$priority][$key]);
}
}
}
return $clone;
}
}
}
@@ -134,7 +134,7 @@ class ConfigForm extends ConfigFormBase {
'#states' => array(
'visible' => array(
array(
':input[name="active_toolbar_items[database]' => array('checked' => TRUE),
'input[name="active_toolbar_items[database]"]' => array('checked' => TRUE),
),
),
),
@@ -31,7 +31,7 @@ class TraceableViewExecutable extends ViewExecutable {
* @return float
*/
public function getExecuteTime() {
return $this->execute_time;
return property_exists($this, 'execute_time') ? $this->execute_time : 0.0;
}
/**
@@ -9,8 +9,8 @@ tags:
dependencies:
- devel
# Information added by Drupal.org packaging script on 2017-08-14
version: '8.x-1.0'
# Information added by Drupal.org packaging script on 2017-10-05
version: '8.x-1.2'
core: '8.x'
project: 'devel'
datestamp: 1502732047
datestamp: 1507197848
@@ -38,7 +38,7 @@ before_script:
# Export database variable for kernel tests.
- export SIMPLETEST_DB=mysql://root:@127.0.0.1/entity
# Download Drupal 8 core.
- travis_retry git clone --branch 8.1.x --depth 1 http://git.drupal.org/project/drupal.git
- travis_retry git clone --branch 8.3.x --depth 1 http://git.drupal.org/project/drupal.git
- cd drupal
- composer self-update
- composer install -n
@@ -5,6 +5,6 @@
"homepage": "http://drupal.org/project/entity",
"license": "GPL-2.0+",
"require": {
"drupal/core": "~8.1"
"drupal/core": "~8.3"
}
}
@@ -3,10 +3,10 @@ description: Provides expanded entity APIs, which will be moved to Drupal core o
type: module
# core: 8.x
dependencies:
- system (>=8.1.0)
- drupal:system (>=8.3.0)
# Information added by Drupal.org packaging script on 2016-12-08
version: '8.x-1.0-alpha4'
# Information added by Drupal.org packaging script on 2017-09-20
version: '8.x-1.0-beta1'
core: '8.x'
project: 'entity'
datestamp: 1481194986
datestamp: 1505895847
@@ -4,3 +4,85 @@
* @file
* Provides expanded entity APIs.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\entity\BundlePlugin\BundlePluginHandler;
/**
* Gets the entity types which use bundle plugins.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* The entity types.
*/
function entity_get_bundle_plugin_entity_types() {
$entity_types = \Drupal::entityTypeManager()->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->hasHandlerClass('bundle_plugin');
});
return $entity_types;
}
/**
* Implements hook_entity_type_build().
*/
function entity_entity_type_build(array &$entity_types) {
foreach ($entity_types as $entity_type) {
if ($entity_type->get('bundle_plugin_type')) {
$entity_type->setHandlerClass('bundle_plugin', BundlePluginHandler::class);
}
}
}
/**
* Implements hook_entity_bundle_info().
*/
function entity_entity_bundle_info() {
$bundles = [];
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
$bundles[$entity_type->id()] = $bundle_handler->getBundleInfo();
}
return $bundles;
}
/**
* Implements hook_entity_field_storage_info().
*/
function entity_entity_field_storage_info(EntityTypeInterface $entity_type) {
if ($entity_type->hasHandlerClass('bundle_plugin')) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
return $bundle_handler->getFieldStorageDefinitions();
}
}
/**
* Implements hook_entity_bundle_field_info().
*/
function entity_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) {
if ($entity_type->hasHandlerClass('bundle_plugin')) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = \Drupal::entityTypeManager()->getHandler($entity_type->id(), 'bundle_plugin');
return $bundle_handler->getFieldDefinitions($bundle);
}
}
/**
* Implements hook_modules_installed().
*/
function entity_modules_installed($modules) {
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
\Drupal::service('entity.bundle_plugin_installer')->installBundles($entity_type, $modules);
}
}
/**
* Implements hook_module_preuninstall().
*/
function entity_module_preuninstall($module) {
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
\Drupal::service('entity.bundle_plugin_installer')->uninstallBundles($entity_type, [$module]);
}
}
@@ -1,6 +1,16 @@
services:
access_checker.entity_revision:
class: \Drupal\entity\Access\EntityRevisionRouteAccessChecker
arguments: ['@entity_type.manager', '@request_stack']
arguments: ['@entity_type.manager', '@current_route_match']
tags:
- { name: access_check, applies_to: _entity_access_revision }
entity.bundle_plugin_installer:
class: Drupal\entity\BundlePlugin\BundlePluginInstaller
arguments: ['@entity_type.manager', '@entity_bundle.listener', '@field_storage_definition.listener', '@field_definition.listener']
entity.bundle_plugin.uninstall_validator:
class: \Drupal\entity\BundlePlugin\BundlePluginUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@entity_type.manager', '@string_translation']
@@ -9,7 +9,7 @@ use Drupal\Core\Entity\EntityTypeInterface;
function entity_views_data() {
$entity_types = \Drupal::entityTypeManager()->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->isSubclassOf(ContentEntityInterface::class);
return $entity_type->entityClassImplements(ContentEntityInterface::class);
});
$data = [];
@@ -7,9 +7,8 @@ use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Route;
/**
@@ -30,42 +29,42 @@ class EntityRevisionRouteAccessChecker implements AccessInterface {
protected $accessCache = array();
/**
* The request stack.
* The currently active route match object.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $requestStack;
protected $routeMatch;
/**
* Creates a new EntityRevisionRouteAccessChecker instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The currently active route match object.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match) {
$this->entityTypeManager = $entity_type_manager;
$this->requestStack = $request_stack;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function access(Route $route, AccountInterface $account, Request $request = NULL) {
if (empty($request)) {
$request = $this->requestStack->getCurrentRequest();
public function access(Route $route, AccountInterface $account, RouteMatchInterface $route_match = NULL) {
if (empty($route_match)) {
$route_match = $this->routeMatch;
}
$operation = $route->getRequirement('_entity_access_revision');
list(, $operation) = explode('.', $operation, 2);
list($entity_type_id, $operation) = explode('.', $operation, 2);
if ($operation === 'list') {
$_entity = $request->attributes->get('_entity', $request->attributes->get($route->getOption('entity_type_id')));
$_entity = $route_match->getParameter($entity_type_id);
return AccessResult::allowedIf($this->checkAccess($_entity, $account, $operation))->cachePerPermissions();
}
else {
$_entity_revision = $request->attributes->get('_entity_revision');
$_entity_revision = $route_match->getParameter($entity_type_id . '_revision');
return AccessResult::allowedIf($_entity_revision && $this->checkAccess($_entity_revision, $account, $operation))->cachePerPermissions();
}
}
@@ -104,8 +103,10 @@ class EntityRevisionRouteAccessChecker implements AccessInterface {
$cid = $entity->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $operation;
if (!isset($this->accessCache[$cid])) {
$admin_permission = $entity_type->getAdminPermission();
// Perform basic permission checks first.
if (!$account->hasPermission($map[$operation]) && !$account->hasPermission($type_map[$operation]) && !$account->hasPermission('administer nodes')) {
if (!$account->hasPermission($map[$operation]) && !$account->hasPermission($type_map[$operation]) && ($admin_permission && !$account->hasPermission($admin_permission))) {
$this->accessCache[$cid] = FALSE;
return FALSE;
}
@@ -114,6 +115,8 @@ class EntityRevisionRouteAccessChecker implements AccessInterface {
$this->accessCache[$cid] = TRUE;
}
else {
// Entity access handlers are generally not aware of the "list" operation.
$operation = $operation == 'list' ? 'view' : $operation;
// First check the access to the default revision and finally, if the
// node passed in is not the default revision then access to that, too.
$this->accessCache[$cid] = $entity_access->access($entity_storage->load($entity->id()), $operation, $account) && ($entity->isDefaultRevision() || $entity_access->access($entity, $operation, $account));
@@ -0,0 +1,27 @@
<?php
namespace Drupal\entity;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Provides a field definition class for bundle fields.
*
* Core currently doesn't provide one, the hook_entity_bundle_field_info()
* example uses BaseFieldDefinition, which is wrong. Tracked in #2346347.
*
* Note that this class implements both FieldStorageDefinitionInterface and
* FieldDefinitionInterface. This is a simplification for DX reasons,
* allowing code to return just the bundle definitions instead of having to
* return both storage definitions and bundle definitions.
*/
class BundleFieldDefinition extends BaseFieldDefinition {
/**
* {@inheritdoc}
*/
public function isBaseField() {
return FALSE;
}
}
@@ -0,0 +1,102 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BundlePluginHandler implements BundlePluginHandlerInterface {
/**
* The entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The bundle plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* Constructs a new BundlePluginHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
* The bundle plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $plugin_manager) {
$this->entityType = $entity_type;
$this->pluginManager = $plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('plugin.manager.' . $entity_type->get('bundle_plugin_type'))
);
}
/**
* {@inheritdoc}
*/
public function getBundleInfo() {
$bundles = [];
foreach ($this->pluginManager->getDefinitions() as $plugin_id => $definition) {
$bundles[$plugin_id] = [
'label' => $definition['label'],
'description' => isset($definition['description']) ? $definition['description'] : '',
'translatable' => $this->entityType->isTranslatable(),
'provider' => $definition['provider'],
];
}
return $bundles;
}
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinitions() {
$definitions = [];
foreach (array_keys($this->pluginManager->getDefinitions()) as $plugin_id) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */
$plugin = $this->pluginManager->createInstance($plugin_id);
$definitions += $plugin->buildFieldDefinitions();
}
// Ensure the presence of required keys which aren't set by the plugin.
foreach ($definitions as $field_name => $definition) {
$definition->setName($field_name);
$definition->setTargetEntityTypeId($this->entityType->id());
$definitions[$field_name] = $definition;
}
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getFieldDefinitions($bundle) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginInterface $plugin */
$plugin = $this->pluginManager->createInstance($bundle);
$definitions = $plugin->buildFieldDefinitions();
// Ensure the presence of required keys which aren't set by the plugin.
foreach ($definitions as $field_name => $definition) {
$definition->setName($field_name);
$definition->setTargetEntityTypeId($this->entityType->id());
$definition->setTargetBundle($bundle);
$definitions[$field_name] = $definition;
}
return $definitions;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Core\Entity\EntityHandlerInterface;
/**
* Handles plugin-provided bundles.
*/
interface BundlePluginHandlerInterface extends EntityHandlerInterface {
/**
* Gets the bundle info.
*
* @return array
* An array of bundle information keyed by the bundle name.
* The format expected by hook_entity_bundle_info().
*/
public function getBundleInfo();
/**
* Gets the field storage definitions.
*/
public function getFieldStorageDefinitions();
/**
* Gets the field definitions for a specific bundle.
*
* @param string $bundle
* The bundle name.
*
* @return \Drupal\entity\BundleFieldDefinition[]
* An array of bundle field definitions, keyed by field name.
*/
public function getFieldDefinitions($bundle);
}
@@ -0,0 +1,94 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Core\Entity\EntityBundleListenerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionListenerInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
class BundlePluginInstaller implements BundlePluginInstallerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity bundle listener.
*
* @var \Drupal\Core\Entity\EntityBundleListenerInterface
*/
protected $entityBundleListener;
/**
* The field storage definition listener.
*
* @var \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
*/
protected $fieldStorageDefinitionListener;
/**
* The field definition listener.
*
* @var \Drupal\Core\Field\FieldDefinitionListenerInterface
*/
protected $fieldDefinitionListener;
/**
* Constructs a new BundlePluginInstaller object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityBundleListenerInterface $entity_bundle_listener
* The entity bundle listener.
* @param \Drupal\Core\Field\FieldStorageDefinitionListenerInterface $field_storage_definition_listener
* The field storage definition listener.
* @param \Drupal\Core\Field\FieldDefinitionListenerInterface $field_definition_listener
* The field definition listener.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityBundleListenerInterface $entity_bundle_listener, FieldStorageDefinitionListenerInterface $field_storage_definition_listener, FieldDefinitionListenerInterface $field_definition_listener) {
$this->entityTypeManager = $entity_type_manager;
$this->entityBundleListener = $entity_bundle_listener;
$this->fieldStorageDefinitionListener = $field_storage_definition_listener;
$this->fieldDefinitionListener = $field_definition_listener;
}
/**
* {@inheritdoc}
*/
public function installBundles(EntityTypeInterface $entity_type, array $modules) {
$bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin');
$bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) {
return in_array($bundle_info['provider'], $modules, TRUE);
});
foreach (array_keys($bundles) as $bundle) {
$this->entityBundleListener->onBundleCreate($bundle, $entity_type->id());
foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) {
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionCreate($definition);
$this->fieldDefinitionListener->onFieldDefinitionCreate($definition);
}
}
}
/**
* {@inheritdoc}
*/
public function uninstallBundles(EntityTypeInterface $entity_type, array $modules) {
$bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin');
$bundles = array_filter($bundle_handler->getBundleInfo(), function ($bundle_info) use ($modules) {
return in_array($bundle_info['provider'], $modules, TRUE);
});
foreach (array_keys($bundles) as $bundle) {
$this->entityBundleListener->onBundleDelete($bundle, $entity_type->id());
foreach ($bundle_handler->getFieldDefinitions($bundle) as $definition) {
$this->fieldDefinitionListener->onFieldDefinitionDelete($definition);
$this->fieldStorageDefinitionListener->onFieldStorageDefinitionDelete($definition);
}
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Installs and uninstalls bundle plugins.
*
* Ensures that the fields provided by the bundle plugins are created/deleted.
*/
interface BundlePluginInstallerInterface {
/**
* Installs the bundle plugins provided by the specified modules.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param array $modules
* The modules.
*/
public function installBundles(EntityTypeInterface $entity_type, array $modules);
/**
* Uninstalls the bundle plugins provided by the specified modules.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param array $modules
* The modules.
*/
public function uninstallBundles(EntityTypeInterface $entity_type, array $modules);
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface for plugins which act as entity bundles.
*/
interface BundlePluginInterface extends PluginInspectionInterface {
/**
* Builds the field definitions for entities of this bundle.
*
* Important:
* Field names must be unique across all bundles.
* It is recommended to prefix them with the bundle name (plugin ID).
*
* @return \Drupal\entity\BundleFieldDefinition[]
* An array of bundle field definitions, keyed by field name.
*/
public function buildFieldDefinitions();
}
@@ -0,0 +1,76 @@
<?php
namespace Drupal\entity\BundlePlugin;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Prevents uninstalling modules with bundle plugins in case of found data.
*/
class BundlePluginUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs the object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];
foreach (entity_get_bundle_plugin_entity_types() as $entity_type) {
/** @var \Drupal\entity\BundlePlugin\BundlePluginHandler $bundle_handler */
$bundle_handler = $this->entityTypeManager->getHandler($entity_type->id(), 'bundle_plugin');
$bundles = $bundle_handler->getBundleInfo();
// We find all bundles which have to be removed due to the uninstallation.
$bundles_filtered_by_module = array_filter($bundles, function ($bundle_info) use ($module) {
return $module === $bundle_info['provider'];
});
if (!empty($bundles_filtered_by_module)) {
$bundle_keys_with_content = array_filter(array_keys($bundles_filtered_by_module), function ($bundle) use ($entity_type) {
$result = $this->entityTypeManager->getStorage($entity_type->id())->getQuery()
->condition($entity_type->getKey('bundle'), $bundle)
->range(0, 1)
->execute();
return !empty($result);
});
$bundles_with_content = array_intersect_key($bundles_filtered_by_module, array_flip($bundle_keys_with_content));
foreach ($bundles_with_content as $bundle) {
$reasons[] = $this->t('There is data for the bundle @bundle on the entity type @entity_type. Please remove all content before uninstalling the module.', [
'@bundle' => $bundle['label'],
'@entity_type' => $entity_type->getLabel(),
]);
}
}
}
return $reasons;
}
}
@@ -11,7 +11,6 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -5,16 +5,30 @@ namespace Drupal\entity;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
/**
* Controls access based on the generic entity permissions.
*
* @see \Drupal\entity\EntityPermissionProvider
* @see \Drupal\entity\UncacheableEntityPermissionProvider
*/
class EntityAccessControlHandler extends CoreEntityAccessControlHandler {
/**
* {@inheritdoc}
*/
public function __construct(EntityTypeInterface $entity_type) {
parent::__construct($entity_type);
if (!$entity_type->hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), EntityPermissionProvider::class, TRUE)) {
throw new \Exception("This entity access control handler requires the entity permissions provider: {EntityPermissionProvider::class}");
}
}
/**
* {@inheritdoc}
*/
@@ -51,10 +65,18 @@ class EntityAccessControlHandler extends CoreEntityAccessControlHandler {
* The access result.
*/
protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
return AccessResult::allowedIfHasPermissions($account, [
"$operation {$entity->getEntityTypeId()}",
"$operation {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
if ($operation === 'view') {
$permissions = [
"view {$entity->getEntityTypeId()}"
];
}
else {
$permissions = [
"$operation {$entity->getEntityTypeId()}",
"$operation {$entity->bundle()} {$entity->getEntityTypeId()}",
];
}
return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
}
/**
@@ -72,23 +94,39 @@ class EntityAccessControlHandler extends CoreEntityAccessControlHandler {
* The access result.
*/
protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\Core\Entity\EntityInterface|\Drupal\user\EntityOwnerInterface $entity */
if (($account->id() == $entity->getOwnerId())) {
$result = AccessResult::allowedIfHasPermissions($account, [
"$operation own {$entity->getEntityTypeId()}",
"$operation any {$entity->getEntityTypeId()}",
"$operation own {$entity->bundle()} {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
if ($operation === 'view') {
if ($entity instanceof EntityPublishedInterface && !$entity->isPublished()) {
if (($account->id() == $entity->getOwnerId())) {
$permissions = [
"view own unpublished {$entity->getEntityTypeId()}",
];
return AccessResult::allowedIfHasPermissions($account, $permissions)->cachePerUser();
}
return AccessResult::neutral()->cachePerUser();
}
else {
return AccessResult::allowedIfHasPermissions($account, [
"view {$entity->getEntityTypeId()}",
]);
}
}
else {
$result = AccessResult::allowedIfHasPermissions($account, [
"$operation any {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
if (($account->id() == $entity->getOwnerId())) {
$result = AccessResult::allowedIfHasPermissions($account, [
"$operation own {$entity->getEntityTypeId()}",
"$operation any {$entity->getEntityTypeId()}",
"$operation own {$entity->bundle()} {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
}
else {
$result = AccessResult::allowedIfHasPermissions($account, [
"$operation any {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
}
return $result;
}
return $result->cachePerUser();
}
/**
@@ -2,8 +2,8 @@
namespace Drupal\entity;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -11,10 +11,21 @@ use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides generic entity permissions.
* Provides generic entity permissions which are still cacheable.
*
* Supports both entity_type and bundle granularities.
* Supports entity ownership (own/any permissions).
* This includes:
*
* - administer $entity_type
* - access $entity_type overview
* - view $entity_type
* - view own unpublished $entity_type
* - update (own|any) ($bundle) $entity_type
* - delete (own|any) ($bundle) $entity_type
* - create $bundle $entity_type
*
* This class does not support "view own ($bundle) $entity_type", because this
* results in caching per user. If you need this use case, please use
* \Drupal\entity\UncacheableEntityPermissionProvider instead.
*
* Intended for content entity types, since config entity types usually rely
* on a single "administer" permission.
@@ -29,218 +40,26 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @see \Drupal\entity\EntityAccessControlHandler
* @see \Drupal\entity\EntityPermissions
*/
class EntityPermissionProvider implements EntityPermissionProviderInterface, EntityHandlerInterface {
use StringTranslationTrait;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a new EntityPermissionProvider object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.bundle.info')
);
}
class EntityPermissionProvider extends EntityPermissionProviderBase {
/**
* {@inheritdoc}
*/
public function buildPermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class);
$singular_label = $entity_type->getSingularLabel();
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
$permissions["administer {$entity_type_id}"] = [
'title' => $this->t('Administer @type', ['@type' => $plural_label]),
'restrict access' => TRUE,
];
$permissions["access {$entity_type_id} overview"] = [
'title' => $this->t('Access the @type overview page', ['@type' => $plural_label]),
];
$permissions = parent::buildPermissions($entity_type);
// View permissions are the same for both granularities.
if ($has_owner) {
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $singular_label,
]),
];
$permissions["view own {$entity_type_id}"] = [
'title' => $this->t('View own @type', [
'@type' => $plural_label,
]),
];
}
else {
$permissions["view {$entity_type_id}"] = [
'title' => $this->t('View @type', [
'@type' => $plural_label,
]),
];
}
// Generate the other permissions based on granularity.
if ($entity_type->getPermissionGranularity() == 'entity_type') {
$permissions += $this->buildEntityTypePermissions($entity_type);
}
else {
$permissions += $this->buildBundlePermissions($entity_type);
}
foreach ($permissions as $name => $permission) {
// Permissions are grouped by provider on admin/people/permissions.
$permissions[$name]['provider'] = $entity_type->getProvider();
// TranslatableMarkup objects don't sort properly.
$permissions[$name]['title'] = (string) $permission['title'];
}
return $permissions;
}
/**
* Builds permissions for the entity_type granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class);
$singular_label = $entity_type->getSingularLabel();
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
$permissions["create {$entity_type_id}"] = [
'title' => $this->t('Create @type', [
$permissions["view {$entity_type_id}"] = [
'title' => $this->t('View @type', [
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["update any {$entity_type_id}"] = [
'title' => $this->t('Update any @type', [
'@type' => $singular_label,
]),
];
$permissions["update own {$entity_type_id}"] = [
'title' => $this->t('Update own @type', [
'@type' => $plural_label,
]),
];
$permissions["delete any {$entity_type_id}"] = [
'title' => $this->t('Delete any @type', [
'@type' => $singular_label,
]),
];
$permissions["delete own {$entity_type_id}"] = [
'title' => $this->t('Delete own @type', [
'@type' => $plural_label,
]),
];
}
else {
$permissions["update {$entity_type_id}"] = [
'title' => $this->t('Update @type', [
'@type' => $plural_label,
]),
];
$permissions["delete {$entity_type_id}"] = [
'title' => $this->t('Delete @type', [
'@type' => $plural_label,
]),
];
}
return $permissions;
return $this->processPermissions($permissions, $entity_type);
}
/**
* Builds permissions for the bundle granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildBundlePermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class);
$singular_label = $entity_type->getSingularLabel();
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
foreach ($bundles as $bundle_name => $bundle_info) {
$permissions["create {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Create @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["update any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $singular_label,
]),
];
$permissions["update own {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update own @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
$permissions["delete any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $singular_label,
]),
];
$permissions["delete own {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete own @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
else {
$permissions["update {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
$permissions["delete {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
}
return $permissions;
}
}
@@ -0,0 +1,231 @@
<?php
namespace Drupal\entity;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @internal
*/
class EntityPermissionProviderBase implements EntityPermissionProviderInterface, EntityHandlerInterface {
use StringTranslationTrait;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a new EntityPermissionProvider object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function buildPermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
$permissions["administer {$entity_type_id}"] = [
'title' => $this->t('Administer @type', ['@type' => $plural_label]),
'restrict access' => TRUE,
];
$permissions["access {$entity_type_id} overview"] = [
'title' => $this->t('Access the @type overview page', ['@type' => $plural_label]),
];
if ($has_owner && $entity_type->entityClassImplements(EntityPublishedInterface::class)) {
$permissions["view own unpublished {$entity_type_id}"] = [
'title' => $this->t('View own unpublished @type', [
'@type' => $plural_label,
]),
];
}
// Generate the other permissions based on granularity.
if ($entity_type->getPermissionGranularity() === 'entity_type') {
$permissions += $this->buildEntityTypePermissions($entity_type);
}
else {
$permissions += $this->buildBundlePermissions($entity_type);
}
return $this->processPermissions($permissions, $entity_type);
}
/**
* Adds the provider and converts the titles to strings to allow sorting.
*
* @param array $permissions
* The array of permissions
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* An array of processed permissions.
*/
protected function processPermissions(array $permissions, EntityTypeInterface $entity_type) {
foreach ($permissions as $name => $permission) {
// Permissions are grouped by provider on admin/people/permissions.
$permissions[$name]['provider'] = $entity_type->getProvider();
// TranslatableMarkup objects don't sort properly.
$permissions[$name]['title'] = (string) $permission['title'];
}
return $permissions;
}
/**
* Builds permissions for the entity_type granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$singular_label = $entity_type->getSingularLabel();
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
$permissions["create {$entity_type_id}"] = [
'title' => $this->t('Create @type', [
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["update any {$entity_type_id}"] = [
'title' => $this->t('Update any @type', [
'@type' => $singular_label,
]),
];
$permissions["update own {$entity_type_id}"] = [
'title' => $this->t('Update own @type', [
'@type' => $plural_label,
]),
];
$permissions["delete any {$entity_type_id}"] = [
'title' => $this->t('Delete any @type', [
'@type' => $singular_label,
]),
];
$permissions["delete own {$entity_type_id}"] = [
'title' => $this->t('Delete own @type', [
'@type' => $plural_label,
]),
];
}
else {
$permissions["update {$entity_type_id}"] = [
'title' => $this->t('Update @type', [
'@type' => $plural_label,
]),
];
$permissions["delete {$entity_type_id}"] = [
'title' => $this->t('Delete @type', [
'@type' => $plural_label,
]),
];
}
return $permissions;
}
/**
* Builds permissions for the bundle granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildBundlePermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$singular_label = $entity_type->getSingularLabel();
$plural_label = $entity_type->getPluralLabel();
$permissions = [];
foreach ($bundles as $bundle_name => $bundle_info) {
$permissions["create {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Create @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["update any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $singular_label,
]),
];
$permissions["update own {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update own @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
$permissions["delete any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $singular_label,
]),
];
$permissions["delete own {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete own @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
else {
$permissions["update {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Update @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
$permissions["delete {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: Delete @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
}
return $permissions;
}
}
@@ -10,6 +10,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* Generates entity permissions via their permission providers.
*
* @see \Drupal\entity\EntityPermissionProvider
* @see \Drupal\entity\UncacheableEntityPermissionProvider
*/
class EntityPermissions implements ContainerInjectionInterface {
@@ -69,7 +69,7 @@ class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterfa
protected function getParticipatingEntityTypes() {
$entity_types = $this->entityTypeManager->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->isSubclassOf(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form');
return $entity_type->entityClassImplements(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form');
});
return $entity_types;
@@ -3,6 +3,7 @@
namespace Drupal\entity\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -24,7 +25,7 @@ class RevisionsOverviewDeriver extends DeriverBase implements ContainerDeriverIn
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(\Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager) {
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->entityTypeManager = $entityTypeManager;
}
@@ -55,7 +56,7 @@ class RevisionsOverviewDeriver extends DeriverBase implements ContainerDeriverIn
$this->derivatives[$entity_type_id] = [
'route_name' => "entity.$entity_type_id.version_history",
'title' => 'Revisions',
'title' => t('Revisions'),
'base_route' => "entity.$entity_type_id.canonical",
'weight' => 20,
] + $base_plugin_definition;
@@ -3,7 +3,6 @@
namespace Drupal\entity\Revision;
use Drupal\Core\Entity\RevisionableContentEntityBase as BaseRevisionableContentEntityBase;
use Drupal\Core\Entity\ContentEntityBase;
/**
* Improves the url route handling of core's revisionable content entity base.
@@ -0,0 +1,138 @@
<?php
namespace Drupal\entity;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
/**
* Controls access based on the uncacheable entity permissions.
*
* @see \Drupal\entity\UncacheableEntityPermissionProvider
*
* Note: this access control handler will cause pages to be cached per user.
*/
class UncacheableEntityAccessControlHandler extends CoreEntityAccessControlHandler {
/**
* {@inheritdoc}
*/
public function __construct(EntityTypeInterface $entity_type) {
parent::__construct($entity_type);
if (!$entity_type->hasHandlerClass('permission_provider') || !is_a($entity_type->getHandlerClass('permission_provider'), UncacheableEntityPermissionProvider::class, TRUE)) {
throw new \Exception("This entity access control handler requires the entity permissions provider: {EntityPermissionProvider::class}");
}
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
$account = $this->prepareUser($account);
/** @var \Drupal\Core\Access\AccessResult $result */
$result = parent::checkAccess($entity, $operation, $account);
if ($result->isNeutral()) {
if ($entity instanceof EntityOwnerInterface) {
$result = $this->checkEntityOwnerPermissions($entity, $operation, $account);
}
else {
$result = $this->checkEntityPermissions($entity, $operation, $account);
}
}
// Ensure that access is evaluated again when the entity changes.
return $result->addCacheableDependency($entity);
}
/**
* Checks the entity operation and bundle permissions.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check access.
* @param string $operation
* The entity operation. Usually one of 'view', 'view label', 'update' or
* 'delete'.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
return AccessResult::allowedIfHasPermissions($account, [
"$operation {$entity->getEntityTypeId()}",
"$operation {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
}
/**
* Checks the entity operation and bundle permissions, with owners.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which to check access.
* @param string $operation
* The entity operation. Usually one of 'view', 'view label', 'update' or
* 'delete'.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\Core\Entity\EntityInterface|\Drupal\user\EntityOwnerInterface $entity */
if (($account->id() == $entity->getOwnerId())) {
if ($operation === 'view' && $entity instanceof EntityPublishedInterface && !$entity->isPublished()) {
$permissions = [
"view own unpublished {$entity->getEntityTypeId()}",
];
}
else {
$permissions = [
"$operation own {$entity->getEntityTypeId()}",
"$operation any {$entity->getEntityTypeId()}",
"$operation own {$entity->bundle()} {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
];
}
$result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
}
else {
$result = AccessResult::allowedIfHasPermissions($account, [
"$operation any {$entity->getEntityTypeId()}",
"$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
], 'OR');
}
return $result->cachePerUser();
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
$result = parent::checkCreateAccess($account, $context, $entity_bundle);
if ($result->isNeutral()) {
$permissions = [
'administer ' . $this->entityTypeId,
'create ' . $this->entityTypeId,
];
if ($entity_bundle) {
$permissions[] = 'create ' . $entity_bundle . ' ' . $this->entityTypeId;
}
$result = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
}
return $result;
}
}
@@ -0,0 +1,143 @@
<?php
namespace Drupal\entity;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides generic entity permissions which are cached per user.
*
* This includes:
*
* - administer $entity_type
* - access $entity_type overview
* - view an ($bundle) $entity_type
* - view own ($bundle) $entity_type
* - view own unpublished $entity_type
* - update (own|any) ($bundle) $entity_type
* - delete (own|any) ($bundle) $entity_type
* - create $bundle $entity_type
*
* As this class supports "view own ($bundle) $entity_type" it is just cacheable
* per user, which might harm performance of sites. Given that please use
* \Drupal\entity\EntityPermissionProvider unless you need the feature, or your
* entity type is not really user facing (commerce orders for example).
*
* Intended for content entity types, since config entity types usually rely
* on a single "administer" permission.
* Example annotation:
* @code
* handlers = {
* "access" = "Drupal\entity\UncacheableEntityAccessControlHandler",
* "permission_provider" = "Drupal\entity\UncacheableEntityPermissionProvider",
* }
* @endcode
*
* @see \Drupal\entity\EntityAccessControlHandler
* @see \Drupal\entity\EntityPermissions
*/
class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase {
/**
* Builds permissions for the entity_type granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) {
$permissions = parent::buildEntityTypePermissions($entity_type);
$entity_type_id = $entity_type->id();
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$plural_label = $entity_type->getPluralLabel();
if ($has_owner) {
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $plural_label,
]),
];
$permissions["view own {$entity_type_id}"] = [
'title' => $this->t('View own @type', [
'@type' => $plural_label,
]),
];
}
else {
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $plural_label,
]),
];
}
return $permissions;
}
/**
* Builds permissions for the bundle granularity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The permissions.
*/
protected function buildBundlePermissions(EntityTypeInterface $entity_type) {
$permissions = parent::buildBundlePermissions($entity_type);
$entity_type_id = $entity_type->id();
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$plural_label = $entity_type->getPluralLabel();
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["view own {$entity_type_id}"] = [
'title' => $this->t('View own @type', [
'@type' => $plural_label,
]),
];
}
foreach ($bundles as $bundle_name => $bundle_info) {
if ($has_owner) {
$permissions["view any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: View any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
$permissions["view own {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: View own @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
else {
$permissions["view any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: View any @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),
];
}
}
return $permissions;
}
}
@@ -0,0 +1,13 @@
name: 'Entity bundle plugin examples test'
type: module
description: 'Module for testing bundle plugins.'
package: Testing
# core: 8.x
dependencies:
- entity
# Information added by Drupal.org packaging script on 2017-09-20
version: '8.x-1.0-beta1'
core: '8.x'
project: 'entity'
datestamp: 1505895847
@@ -0,0 +1,31 @@
<?php
namespace Drupal\entity_module_bundle_plugin_examples_test\Plugin\BundlePluginTest;
use Drupal\entity\BundleFieldDefinition;
use Drupal\Core\Plugin\PluginBase;
use Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface;
/**
* Provides the second bundle plugin.
*
* @BundlePluginTest(
* id = "second",
* label = @Translation("Second"),
* )
*/
class Second extends PluginBase implements BundlePluginTestInterface {
/**
* {@inheritdoc}
*/
public function buildFieldDefinitions() {
$fields = [];
$fields['second_mail'] = BundleFieldDefinition::create('email')
->setLabel(t('Email'))
->setRequired(TRUE);
return $fields;
}
}
@@ -0,0 +1,13 @@
name: 'Entity bundle plugin test'
type: module
description: 'Module for testing bundle plugins.'
package: Testing
# core: 8.x
dependencies:
- entity
# Information added by Drupal.org packaging script on 2017-09-20
version: '8.x-1.0-beta1'
core: '8.x'
project: 'entity'
datestamp: 1505895847
@@ -0,0 +1,4 @@
services:
plugin.manager.bundle_plugin_test:
class: Drupal\entity_module_bundle_plugin_test\BundlePluginTestManager
parent: default_plugin_manager
@@ -0,0 +1,34 @@
<?php
namespace Drupal\entity_module_bundle_plugin_test\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines the BundlePluginTest annotation object.
*
* Plugin namespace: Plugin\BundlePluginTest.
*
* @see plugin_api
*
* @Annotation
*/
class BundlePluginTest extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin label.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\entity_module_bundle_plugin_test;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages discovery and instantiation of BundlePluginTest plugins.
*
* @see \Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest
* @see plugin_api
*/
class BundlePluginTestManager extends DefaultPluginManager {
/**
* Constructs a new BundlePluginTestManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/BundlePluginTest', $namespaces, $module_handler, 'Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest\BundlePluginTestInterface', 'Drupal\entity_module_bundle_plugin_test\Annotation\BundlePluginTest');
$this->alterInfo('bundle_plugin_test_info');
$this->setCacheBackend($cache_backend, 'bundle_plugin_test_plugins');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\entity_module_bundle_plugin_test\Entity;
use Drupal\Core\Entity\ContentEntityBase;
/**
* Defines the 'entity_test_bundle_plugin' entity class.
*
* @ContentEntityType(
* id = "entity_test_bundle_plugin",
* label = @Translation("Entity test bundle plugin"),
* bundle_label = @Translation("Bundle Plugin Test"),
* bundle_plugin_type = "bundle_plugin_test",
* base_table = "entity_test_bundle_plugin",
* admin_permission = "administer content",
* fieldable = TRUE,
* entity_keys = {
* "id" = "method_id",
* "uuid" = "uuid",
* "bundle" = "type"
* },
* )
*/
class EntityTestBundlePlugin extends ContentEntityBase {
}
@@ -0,0 +1,12 @@
<?php
namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest;
use Drupal\entity\BundlePlugin\BundlePluginInterface;
/**
* Defines the interface for BundlePluginTest plugins.
*/
interface BundlePluginTestInterface extends BundlePluginInterface {
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\entity_module_bundle_plugin_test\Plugin\BundlePluginTest;
use Drupal\entity\BundleFieldDefinition;
use Drupal\Core\Plugin\PluginBase;
/**
* Provides the first bundle plugin.
*
* @BundlePluginTest(
* id = "first",
* label = @Translation("First"),
* description = @Translation("Some description"),
* )
*/
class First extends PluginBase implements BundlePluginTestInterface {
/**
* {@inheritdoc}
*/
public function buildFieldDefinitions() {
$fields = [];
$fields['first_mail'] = BundleFieldDefinition::create('email')
->setLabel(t('Email'))
->setRequired(TRUE);
return $fields;
}
}
@@ -1,9 +1,10 @@
name: Entity test
type: module
package: Testing
# core: 8.x
# Information added by Drupal.org packaging script on 2016-12-08
version: '8.x-1.0-alpha4'
# Information added by Drupal.org packaging script on 2017-09-20
version: '8.x-1.0-beta1'
core: '8.x'
project: 'entity'
datestamp: 1481194986
datestamp: 1505895847
@@ -14,7 +14,7 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
* label = @Translation("Entity test with enhancements"),
* handlers = {
* "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "access" = "\Drupal\entity\EntityAccessControlHandler",
* "access" = "\Drupal\Core\Entity\EntityAccessControlHandler",
* "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
* "form" = {
* "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
@@ -4,7 +4,7 @@ namespace Drupal\Tests\entity\Functional;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\simpletest\BrowserTestBase;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the delete multiple confirmation form.
@@ -0,0 +1,146 @@
<?php
namespace Drupal\Tests\entity\Kernel;
use Drupal\entity_module_bundle_plugin_test\Entity\EntityTestBundlePlugin;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the bundle plugin API.
*
* @group entity
*/
class BundlePluginTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'system',
'entity',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', 'router');
// Install the modules properly. Putting them into static::$modules doesn't trigger the install
// hooks, like hook_modules_installed, so entity_modules_installed is not triggered().
/** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */
$module_installer = \Drupal::service('module_installer');
$module_installer->install(['entity_module_bundle_plugin_test', 'entity_module_bundle_plugin_examples_test']);
}
/**
* Tests the bundle plugins.
*/
public function testPluginBundles() {
$bundled_entity_types = entity_get_bundle_plugin_entity_types();
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
$entity_type = $bundled_entity_types['entity_test_bundle_plugin'];
$this->assertEquals('entity_test_bundle_plugin', $entity_type->id());
$this->assertTrue($entity_type->hasHandlerClass('bundle_plugin'));
/** @var \Drupal\Core\Entity\EntityTypeBundleInfo $entity_type_bundle_info */
$entity_type_bundle_info = \Drupal::service('entity_type.bundle.info');
$bundle_info = $entity_type_bundle_info->getBundleInfo('entity_test_bundle_plugin');
$this->assertEquals(2, count($bundle_info));
$this->assertArrayHasKey('first', $bundle_info);
$this->assertArrayHasKey('second', $bundle_info);
$this->assertEquals('First', $bundle_info['first']['label']);
$this->assertEquals('Some description', $bundle_info['first']['description']);
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions('entity_test_bundle_plugin');
$this->assertArrayHasKey('first_mail', $field_storage_definitions);
$this->assertArrayHasKey('second_mail', $field_storage_definitions);
$first_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'first');
$this->assertArrayHasKey('first_mail', $first_field_definitions);
$this->assertArrayNotHasKey('second_mail', $first_field_definitions);
$second_field_definitions = $entity_field_manager->getFieldDefinitions('entity_test_bundle_plugin', 'second');
$this->assertArrayNotHasKey('first_mail', $second_field_definitions);
$this->assertArrayHasKey('second_mail', $second_field_definitions);
$first_entity = EntityTestBundlePlugin::create([
'type' => 'first',
'first_mail' => 'admin@test.com',
]);
$first_entity->save();
$first_entity = EntityTestBundlePlugin::load($first_entity->id());
$this->assertEquals('admin@test.com', $first_entity->first_mail->value);
$second_entity = EntityTestBundlePlugin::create([
'type' => 'second',
'second_mail' => 'admin@example.com',
]);
$second_entity->save();
$second_entity = EntityTestBundlePlugin::load($second_entity->id());
$this->assertEquals('admin@example.com', $second_entity->second_mail->value);
// Also test entity queries.
$result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin')
->getQuery()
->condition('second_mail', 'admin@example.com')
->execute();
$this->assertEquals([$second_entity->id() => $second_entity->id()], $result);
$result = \Drupal::entityTypeManager()->getStorage('entity_test_bundle_plugin')
->getQuery()
->condition('type', 'first')
->execute();
$this->assertEquals([$first_entity->id() => $first_entity->id()], $result);
}
/**
* Tests the uninstallation for a bundle provided by a module.
*/
public function testBundlePluginModuleUninstallation() {
/** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */
$module_installer = \Drupal::service('module_installer');
// One should be possible to uninstall without any actual content.
$violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']);
$this->assertEmpty($violations);
$first_entity = EntityTestBundlePlugin::create([
'type' => 'first',
'first_mail' => 'admin@test.com',
]);
$first_entity->save();
$second_entity = EntityTestBundlePlugin::create([
'type' => 'second',
'second_mail' => 'admin@example.com',
]);
$second_entity->save();
$violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']);
$this->assertCount(1, $violations);
$this->assertCount(1, $violations['entity_module_bundle_plugin_examples_test']);
$this->assertEquals('There is data for the bundle Second on the entity type Entity test bundle plugin. Please remove all content before uninstalling the module.', $violations['entity_module_bundle_plugin_examples_test'][0]);
$second_entity->delete();
// The first entity is defined by entity_module_bundle_plugin_test, so it should be possible
// to uninstall the module providing the second bundle plugin.
$violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_examples_test']);
$this->assertEmpty($violations);
$module_installer->uninstall(['entity_module_bundle_plugin_examples_test']);
// The first entity is provided by entity_module_bundle_plugin_test which we cannot uninstall,
// until all the entities are deleted.
$violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']);
$this->assertNotEmpty($violations);
$first_entity->delete();
$violations = $module_installer->validateUninstall(['entity_module_bundle_plugin_test']);
$this->assertEmpty($violations);
}
}
@@ -57,22 +57,36 @@ class RevisionBasicUITest extends KernelTestBase {
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */
$http_kernel = \Drupal::service('http_kernel');
$request = Request::create($revision->url('version-history'));
$request = Request::create($revision->toUrl('version-history')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(403, $response->getStatusCode());
$role_admin = Role::create(['id' => 'test_role_admin']);
$role_admin->grantPermission('administer entity_test_enhanced');
$role_admin->save();
$role = Role::create(['id' => 'test_role']);
$role->grantPermission('view all entity_test_enhanced revisions');
$role->grantPermission('administer entity_test_enhanced');
$role->save();
$user_admin = User::create([
'name' => 'Test user admin',
]);
$user_admin->addRole($role_admin->id());
\Drupal::service('account_switcher')->switchTo($user_admin);
$request = Request::create($revision->toUrl('version-history')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$user = User::create([
'name' => 'Test user',
]);
$user->addRole($role->id());
\Drupal::service('account_switcher')->switchTo($user);
$request = Request::create($revision->url('version-history'));
$request = Request::create($revision->toUrl('version-history')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
@@ -87,7 +101,7 @@ class RevisionBasicUITest extends KernelTestBase {
$revision->isDefaultRevision(TRUE);
$revision->save();
$request = Request::create($revision->url('version-history'));
$request = Request::create($revision->toUrl('version-history')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
@@ -110,22 +124,36 @@ class RevisionBasicUITest extends KernelTestBase {
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */
$http_kernel = \Drupal::service('http_kernel');
$request = Request::create($revision->url('revision'));
$request = Request::create($revision->toUrl('revision')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(403, $response->getStatusCode());
$role_admin = Role::create(['id' => 'test_role_admin']);
$role_admin->grantPermission('administer entity_test_enhanced');
$role_admin->save();
$role = Role::create(['id' => 'test_role']);
$role->grantPermission('view all entity_test_enhanced revisions');
$role->grantPermission('administer entity_test_enhanced');
$role->save();
$user_admin = User::create([
'name' => 'Test user admin',
]);
$user_admin->addRole($role_admin->id());
\Drupal::service('account_switcher')->switchTo($user_admin);
$request = Request::create($revision->toUrl('version-history')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$user = User::create([
'name' => 'Test user',
]);
$user->addRole($role->id());
\Drupal::service('account_switcher')->switchTo($user);
$request = Request::create($revision->url('revision'));
$request = Request::create($revision->toUrl('revision')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertNotContains('rev 1', $response->getContent());
@@ -156,7 +184,7 @@ class RevisionBasicUITest extends KernelTestBase {
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel */
$http_kernel = \Drupal::service('http_kernel');
$request = Request::create($entity->url('revision-revert-form'));
$request = Request::create($entity->toUrl('revision-revert-form')->toString());
$response = $http_kernel->handle($request);
$this->assertEquals(200, $response->getStatusCode());
}
@@ -8,12 +8,14 @@ use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity\EntityAccessControlHandler;
use Drupal\entity\EntityPermissionProvider;
use Drupal\Tests\UnitTestCase;
use Drupal\user\EntityOwnerInterface;
use Prophecy\Argument;
@@ -80,6 +82,8 @@ class EntityAccessControlHandlerTest extends UnitTestCase {
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->id()->willReturn('green_entity');
$entity_type->getAdminPermission()->willReturn('administer green_entity');
$entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE);
$entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class);
// User with the admin permission can do anything.
$entity = $this->buildMockEntity($entity_type->reveal());
@@ -126,6 +130,32 @@ class EntityAccessControlHandlerTest extends UnitTestCase {
$data[] = [$entity->reveal(), 'update', $second_account->reveal(), FALSE];
$data[] = [$entity->reveal(), 'update', $third_account->reveal(), TRUE];
// Test the unpublished permissions.
$entity_first_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'first', FALSE);
$entity_first_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'first', FALSE);
$entity_first_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'first', FALSE);
$entity_second_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'second', FALSE);
$entity_second_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'second', FALSE);
$entity_second_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'second', FALSE);
$user_view_own_up = $this->buildMockUser(14, 'view own unpublished green_entity');
$user_view_other = $this->buildMockUser(15, 'view green_entity');
$data['entity_first_other_up user_view_own_up'] = [$entity_first_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_first_own_up user_view_own_up'] = [$entity_first_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE];
$data['entity_first_own_bundle_up user_view_own_up'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_second_other_up user_view_own_up'] = [$entity_second_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_second_own_up user_view_own_up'] = [$entity_second_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE];
$data['entity_second_own_bundle_up user_view_own_up'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_first_other_up user_view_other'] = [$entity_first_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_first_own_up user_view_other'] = [$entity_first_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_first_own_bundle_up user_view_other'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_other_up user_view_other'] = [$entity_second_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_own_up user_view_other'] = [$entity_second_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_own_bundle_up user_view_other'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
return $data;
}
@@ -141,6 +171,8 @@ class EntityAccessControlHandlerTest extends UnitTestCase {
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->id()->willReturn('green_entity');
$entity_type->getAdminPermission()->willReturn('administer green_entity');
$entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE);
$entity_type->getHandlerClass('permission_provider')->willReturn(EntityPermissionProvider::class);
// User with the admin permission.
$account = $this->prophesize(AccountInterface::class);
@@ -182,16 +214,27 @@ class EntityAccessControlHandlerTest extends UnitTestCase {
* @return \Prophecy\Prophecy\ObjectProphecy
* The entity mock.
*/
protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL) {
protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) {
$langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$entity = $this->prophesize(ContentEntityInterface::class);
if (isset($published)) {
$entity->willImplement(EntityPublishedInterface::class);
}
if ($owner_id) {
$entity->willImplement(EntityOwnerInterface::class);
}
if (isset($published)) {
$entity->isPublished()->willReturn($published);
}
if ($owner_id) {
$entity->getOwnerId()->willReturn($owner_id);
}
$entity->bundle()->willReturn($entity_type->id());
$entity->bundle()->willReturn($bundle ?: $entity_type->id());
$entity->isNew()->willReturn(FALSE);
$entity->uuid()->willReturn('fake uuid');
$entity->id()->willReturn('fake id');
$entity->getRevisionId()->willReturn(NULL);
$entity->language()->willReturn(new Language(['id' => $langcode]));
$entity->getEntityTypeId()->willReturn($entity_type->id());
$entity->getEntityType()->willReturn($entity_type);
@@ -199,7 +242,16 @@ class EntityAccessControlHandlerTest extends UnitTestCase {
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(Cache::PERMANENT);
return $entity;
}
protected function buildMockUser($uid, $permission) {
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn($uid);
$account->hasPermission($permission)->willReturn(TRUE);
$account->hasPermission(Argument::any())->willReturn(FALSE);
return $account;
}
}
@@ -2,8 +2,8 @@
namespace Drupal\Tests\entity\Unit;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\entity\EntityPermissionProvider;
@@ -37,6 +37,9 @@ class EntityPermissionProviderTest extends UnitTestCase {
$entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([
'third' => ['label' => 'Third'],
]);
$entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([
'third' => ['label' => 'Third'],
]);
$this->permissionProvider = new EntityPermissionProvider($entity_type_bundle_info->reveal());
$this->permissionProvider->setStringTranslation($this->getStringTranslationStub());
}
@@ -69,15 +72,16 @@ class EntityPermissionProviderTest extends UnitTestCase {
$entity_type->id()->willReturn('green_entity');
$entity_type->getSingularLabel()->willReturn('green entity');
$entity_type->getPluralLabel()->willReturn('green entities');
$entity_type->isSubclassOf(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('entity_type');
$expected_permissions = [
'administer green_entity' => 'Administer green entities',
'access green_entity overview' => 'Access the green entities overview page',
'view green_entity' => 'View green entities',
'create green_entity' => 'Create green entities',
'update green_entity' => 'Update green entities',
'delete green_entity' => 'Delete green entities',
'view green_entity' => 'View green entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
@@ -87,18 +91,18 @@ class EntityPermissionProviderTest extends UnitTestCase {
$entity_type->id()->willReturn('blue_entity');
$entity_type->getSingularLabel()->willReturn('blue entity');
$entity_type->getPluralLabel()->willReturn('blue entities');
$entity_type->isSubclassOf(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('entity_type');
$expected_permissions = [
'administer blue_entity' => 'Administer blue entities',
'access blue_entity overview' => 'Access the blue entities overview page',
'view any blue_entity' => 'View any blue entity',
'view own blue_entity' => 'View own blue entities',
'create blue_entity' => 'Create blue entities',
'update any blue_entity' => 'Update any blue entity',
'update own blue_entity' => 'Update own blue entities',
'delete any blue_entity' => 'Delete any blue entity',
'delete own blue_entity' => 'Delete own blue entities',
'view blue_entity' => 'View blue entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
@@ -108,18 +112,19 @@ class EntityPermissionProviderTest extends UnitTestCase {
$entity_type->id()->willReturn('white_entity');
$entity_type->getSingularLabel()->willReturn('white entity');
$entity_type->getPluralLabel()->willReturn('white entities');
$entity_type->isSubclassOf(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer white_entity' => 'Administer white entities',
'access white_entity overview' => 'Access the white entities overview page',
'view white_entity' => 'View white entities',
'create first white_entity' => 'First: Create white entities',
'update first white_entity' => 'First: Update white entities',
'delete first white_entity' => 'First: Delete white entities',
'create second white_entity' => 'Second: Create white entities',
'update second white_entity' => 'Second: Update white entities',
'delete second white_entity' => 'Second: Delete white entities',
'view white_entity' => 'View white entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
@@ -129,18 +134,40 @@ class EntityPermissionProviderTest extends UnitTestCase {
$entity_type->id()->willReturn('black_entity');
$entity_type->getSingularLabel()->willReturn('black entity');
$entity_type->getPluralLabel()->willReturn('black entities');
$entity_type->isSubclassOf(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer black_entity' => 'Administer black entities',
'access black_entity overview' => 'Access the black entities overview page',
'view any black_entity' => 'View any black entity',
'view own black_entity' => 'View own black entities',
'create third black_entity' => 'Third: Create black entities',
'update any third black_entity' => 'Third: Update any black entity',
'update own third black_entity' => 'Third: Update own black entities',
'delete any third black_entity' => 'Third: Delete any black entity',
'delete own third black_entity' => 'Third: Delete own black entities',
'view black_entity' => 'View black entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
// Content entity type with bundles and owner and entity published.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('pink_entity');
$entity_type->getSingularLabel()->willReturn('pink entity');
$entity_type->getPluralLabel()->willReturn('pink entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer pink_entity' => 'Administer pink entities',
'access pink_entity overview' => 'Access the pink entities overview page',
'view own unpublished pink_entity' => 'View own unpublished pink entities',
'create third pink_entity' => 'Third: Create pink entities',
'update any third pink_entity' => 'Third: Update any pink entity',
'update own third pink_entity' => 'Third: Update own pink entities',
'delete any third pink_entity' => 'Third: Delete any pink entity',
'delete own third pink_entity' => 'Third: Delete own pink entities',
'view pink_entity' => 'View pink entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
@@ -0,0 +1,299 @@
<?php
namespace Drupal\Tests\entity\Unit;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity\UncacheableEntityAccessControlHandler;
use Drupal\entity\UncacheableEntityPermissionProvider;
use Drupal\Tests\UnitTestCase;
use Drupal\user\EntityOwnerInterface;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\entity\EntityAccessControlHandler
* @group entity
*/
class UncacheableEntityAccessControlHandlerTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]);
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE);
$container = new ContainerBuilder();
$container->set('module_handler', $module_handler->reveal());
$container->set('cache_contexts_manager', $cache_contexts_manager->reveal());
\Drupal::setContainer($container);
}
/**
* @covers ::checkAccess
* @covers ::checkEntityPermissions
* @covers ::checkEntityOwnerPermissions
* @covers ::checkCreateAccess
*
* @dataProvider accessProvider
*/
public function testAccess(EntityInterface $entity, $operation, $account, $allowed) {
$handler = new UncacheableEntityAccessControlHandler($entity->getEntityType());
$handler->setStringTranslation($this->getStringTranslationStub());
$result = $handler->access($entity, $operation, $account);
$this->assertEquals($allowed, $result);
}
/**
* @covers ::checkCreateAccess
*
* @dataProvider createAccessProvider
*/
public function testCreateAccess(EntityTypeInterface $entity_type, $bundle, $account, $allowed) {
$handler = new UncacheableEntityAccessControlHandler($entity_type);
$handler->setStringTranslation($this->getStringTranslationStub());
$result = $handler->createAccess($bundle, $account);
$this->assertEquals($allowed, $result);
}
/**
* Data provider for testAccess().
*
* @return array
* A list of testAccess method arguments.
*/
public function accessProvider() {
$data = [];
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->id()->willReturn('green_entity');
$entity_type->getAdminPermission()->willReturn('administer green_entity');
$entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE);
$entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class);
// User with the admin permission can do anything.
$entity = $this->buildMockEntity($entity_type->reveal());
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn(6);
$account->hasPermission('administer green_entity')->willReturn(TRUE);
$data[] = [$entity->reveal(), 'view', $account->reveal(), TRUE];
$data[] = [$entity->reveal(), 'update', $account->reveal(), TRUE];
$data[] = [$entity->reveal(), 'delete', $account->reveal(), TRUE];
// Entity with no owner.
$entity = $this->buildMockEntity($entity_type->reveal());
// User who has access.
$first_account = $this->prophesize(AccountInterface::class);
$first_account->id()->willReturn(6);
$first_account->hasPermission('view green_entity')->willReturn(TRUE);
$first_account->hasPermission(Argument::any())->willReturn(FALSE);
// User who doesn't have access.
$second_account = $this->prophesize(AccountInterface::class);
$second_account->id()->willReturn(7);
$second_account->hasPermission('view green_entity')->willReturn(FALSE);
$second_account->hasPermission(Argument::any())->willReturn(FALSE);
$data[] = [$entity->reveal(), 'view', $first_account->reveal(), TRUE];
$data[] = [$entity->reveal(), 'view', $second_account->reveal(), FALSE];
// Entity with owner.
$entity = $this->buildMockEntity($entity_type->reveal(), 6);
// Owner.
$first_account = $this->prophesize(AccountInterface::class);
$first_account->id()->willReturn(6);
$first_account->hasPermission('update own green_entity')->willReturn(TRUE);
$first_account->hasPermission(Argument::any())->willReturn(FALSE);
// Non-owner.
$second_account = $this->prophesize(AccountInterface::class);
$second_account->id()->willReturn(7);
$second_account->hasPermission('update own green_entity')->willReturn(TRUE);
$second_account->hasPermission(Argument::any())->willReturn(FALSE);
// User who can update any.
$third_account = $this->prophesize(AccountInterface::class);
$third_account->id()->willReturn(8);
$third_account->hasPermission('update any green_entity')->willReturn(TRUE);
$third_account->hasPermission(Argument::any())->willReturn(FALSE);
$data[] = [$entity->reveal(), 'update', $first_account->reveal(), TRUE];
$data[] = [$entity->reveal(), 'update', $second_account->reveal(), FALSE];
$data[] = [$entity->reveal(), 'update', $third_account->reveal(), TRUE];
// Per bundle permissions.
$entity_first_other = $this->buildMockEntity($entity_type->reveal(), 9999, 'first');
$entity_first_own = $this->buildMockEntity($entity_type->reveal(), 10, 'first');
$entity_first_own_bundle = $this->buildMockEntity($entity_type->reveal(), 12, 'first');
$entity_second_other = $this->buildMockEntity($entity_type->reveal(), 9999, 'second');
$entity_second_own = $this->buildMockEntity($entity_type->reveal(), 10, 'second');
$entity_second_own_bundle = $this->buildMockEntity($entity_type->reveal(), 12, 'second');
$user_view_any = $this->buildMockUser(9, 'view any green_entity');
$user_view_own = $this->buildMockUser(10, 'view own green_entity');
$user_view_bundle_any = $this->buildMockUser(11, 'view any first green_entity');
$user_view_bundle_own = $this->buildMockUser(12, 'view own first green_entity');
$data['entity_first_other user_view_any'] = [$entity_first_other->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_first_own user_view_any'] = [$entity_first_own->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_first_own_bundle user_view_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_second_other user_view_any'] = [$entity_second_other->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_second_own user_view_any'] = [$entity_second_own->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_second_own_bundle user_view_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_any->reveal(), TRUE];
$data['entity_first_other user_view_own'] = [$entity_first_other->reveal(), 'view', $user_view_own->reveal(), FALSE];
$data['entity_first_own user_view_own'] = [$entity_first_own->reveal(), 'view', $user_view_own->reveal(), TRUE];
$data['entity_first_own_bundle user_view_own'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_own->reveal(), FALSE];
$data['entity_second_other user_view_own'] = [$entity_second_other->reveal(), 'view', $user_view_own->reveal(), FALSE];
$data['entity_second_own user_view_own'] = [$entity_second_own->reveal(), 'view', $user_view_own->reveal(), TRUE];
$data['entity_second_own_bundle user_view_own'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_own->reveal(), FALSE];
$data['entity_first_other user_view_bundle_any'] = [$entity_first_other->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE];
$data['entity_first_own user_view_bundle_any'] = [$entity_first_own->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE];
$data['entity_first_own_bundle user_view_bundle_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_bundle_any->reveal(), TRUE];
$data['entity_second_other user_view_bundle_any'] = [$entity_second_other->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE];
$data['entity_second_own user_view_bundle_any'] = [$entity_second_own->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE];
$data['entity_second_own_bundle user_view_bundle_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_bundle_any->reveal(), FALSE];
$data['entity_first_other user_view_bundle_any'] = [$entity_first_other->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE];
$data['entity_first_own user_view_bundle_any'] = [$entity_first_own->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE];
$data['entity_first_own_bundle user_view_bundle_any'] = [$entity_first_own_bundle->reveal(), 'view', $user_view_bundle_own->reveal(), TRUE];
$data['entity_second_other user_view_bundle_any'] = [$entity_second_other->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE];
$data['entity_second_own user_view_bundle_any'] = [$entity_second_own->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE];
$data['entity_second_own_bundle user_view_bundle_any'] = [$entity_second_own_bundle->reveal(), 'view', $user_view_bundle_own->reveal(), FALSE];
// Test the unpublished permissions.
$entity_first_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'first', FALSE);
$entity_first_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'first', FALSE);
$entity_first_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'first', FALSE);
$entity_second_other_up = $this->buildMockEntity($entity_type->reveal(), 9999, 'second', FALSE);
$entity_second_own_up = $this->buildMockEntity($entity_type->reveal(), 14, 'second', FALSE);
$entity_second_own_bundle_up = $this->buildMockEntity($entity_type->reveal(), 15, 'second', FALSE);
$user_view_own_up = $this->buildMockUser(14, 'view own unpublished green_entity');
$user_view_other = $this->buildMockUser(15, 'view green_entity');
$data['entity_first_other_up user_view_own_up'] = [$entity_first_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_first_own_up user_view_own_up'] = [$entity_first_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE];
$data['entity_first_own_bundle_up user_view_own_up'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_second_other_up user_view_own_up'] = [$entity_second_other_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_second_own_up user_view_own_up'] = [$entity_second_own_up->reveal(), 'view', $user_view_own_up->reveal(), TRUE];
$data['entity_second_own_bundle_up user_view_own_up'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_own_up->reveal(), FALSE];
$data['entity_first_other_up user_view_other'] = [$entity_first_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_first_own_up user_view_other'] = [$entity_first_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_first_own_bundle_up user_view_other'] = [$entity_first_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_other_up user_view_other'] = [$entity_second_other_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_own_up user_view_other'] = [$entity_second_own_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
$data['entity_second_own_bundle_up user_view_other'] = [$entity_second_own_bundle_up->reveal(), 'view', $user_view_other->reveal(), FALSE];
return $data;
}
/**
* Data provider for testCreateAccess().
*
* @return array
* A list of testCreateAccess method arguments.
*/
public function createAccessProvider() {
$data = [];
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->id()->willReturn('green_entity');
$entity_type->getAdminPermission()->willReturn('administer green_entity');
$entity_type->hasHandlerClass('permission_provider')->willReturn(TRUE);
$entity_type->getHandlerClass('permission_provider')->willReturn(UncacheableEntityPermissionProvider::class);
// User with the admin permission.
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn(6);
$account->hasPermission('administer green_entity')->willReturn(TRUE);
$data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE];
// Ordinary user.
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn(6);
$account->hasPermission('create green_entity')->willReturn(TRUE);
$account->hasPermission(Argument::any())->willReturn(FALSE);
$data[] = [$entity_type->reveal(), NULL, $account->reveal(), TRUE];
// Ordinary user, entity with a bundle.
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn(6);
$account->hasPermission('create first_bundle green_entity')->willReturn(TRUE);
$account->hasPermission(Argument::any())->willReturn(FALSE);
$data[] = [$entity_type->reveal(), 'first_bundle', $account->reveal(), TRUE];
// User with no permissions.
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn(6);
$account->hasPermission(Argument::any())->willReturn(FALSE);
$data[] = [$entity_type->reveal(), NULL, $account->reveal(), FALSE];
return $data;
}
/**
* Builds a mock entity.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param string $owner_id
* The owner ID.
*
* @return \Prophecy\Prophecy\ObjectProphecy
* The entity mock.
*/
protected function buildMockEntity(EntityTypeInterface $entity_type, $owner_id = NULL, $bundle = NULL, $published = NULL) {
$langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
$entity = $this->prophesize(ContentEntityInterface::class);
if (isset($published)) {
$entity->willImplement(EntityPublishedInterface::class);
}
if ($owner_id) {
$entity->willImplement(EntityOwnerInterface::class);
}
if (isset($published)) {
$entity->isPublished()->willReturn($published);
}
if ($owner_id) {
$entity->getOwnerId()->willReturn($owner_id);
}
$entity->bundle()->willReturn($bundle ?: $entity_type->id());
$entity->isNew()->willReturn(FALSE);
$entity->uuid()->willReturn('fake uuid');
$entity->id()->willReturn('fake id');
$entity->getRevisionId()->willReturn(NULL);
$entity->language()->willReturn(new Language(['id' => $langcode]));
$entity->getEntityTypeId()->willReturn($entity_type->id());
$entity->getEntityType()->willReturn($entity_type);
$entity->getCacheContexts()->willReturn([]);
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(Cache::PERMANENT);
return $entity;
}
protected function buildMockUser($uid, $permission) {
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn($uid);
$account->hasPermission($permission)->willReturn(TRUE);
$account->hasPermission(Argument::any())->willReturn(FALSE);
return $account;
}
}
@@ -0,0 +1,186 @@
<?php
namespace Drupal\Tests\entity\Unit;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\entity\UncacheableEntityPermissionProvider;
use Drupal\Tests\UnitTestCase;
use Drupal\user\EntityOwnerInterface;
/**
* @coversDefaultClass \Drupal\entity\UncacheableEntityPermissionProvider
* @group entity
*/
class UncacheableEntityPermissionProviderTest extends UnitTestCase {
/**
* The entity permission provider.
*
* @var \Drupal\entity\EntityPermissionProviderInterface
*/
protected $permissionProvider;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
$entity_type_bundle_info->getBundleInfo('white_entity')->willReturn([
'first' => ['label' => 'First'],
'second' => ['label' => 'Second'],
]);
$entity_type_bundle_info->getBundleInfo('black_entity')->willReturn([
'third' => ['label' => 'Third'],
]);
$entity_type_bundle_info->getBundleInfo('pink_entity')->willReturn([
'third' => ['label' => 'Third'],
]);
$this->permissionProvider = new UncacheableEntityPermissionProvider($entity_type_bundle_info->reveal());
$this->permissionProvider->setStringTranslation($this->getStringTranslationStub());
}
/**
* @covers ::buildPermissions
*
* @dataProvider entityTypeProvider
*/
public function testBuildPermissions(EntityTypeInterface $entity_type, array $expected_permissions) {
$permissions = $this->permissionProvider->buildPermissions($entity_type);
$this->assertEquals(array_keys($expected_permissions), array_keys($permissions));
foreach ($permissions as $name => $permission) {
$this->assertEquals('entity_module_test', $permission['provider']);
$this->assertEquals($expected_permissions[$name], $permission['title']);
}
}
/**
* Data provider for testBuildPermissions().
*
* @return array
* A list of testBuildPermissions method arguments.
*/
public function entityTypeProvider() {
$data = [];
// Content entity type.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('green_entity');
$entity_type->getSingularLabel()->willReturn('green entity');
$entity_type->getPluralLabel()->willReturn('green entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('entity_type');
$expected_permissions = [
'administer green_entity' => 'Administer green entities',
'access green_entity overview' => 'Access the green entities overview page',
'create green_entity' => 'Create green entities',
'update green_entity' => 'Update green entities',
'delete green_entity' => 'Delete green entities',
'view any green_entity' => 'View any green entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
// Content entity type with owner.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('blue_entity');
$entity_type->getSingularLabel()->willReturn('blue entity');
$entity_type->getPluralLabel()->willReturn('blue entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('entity_type');
$expected_permissions = [
'administer blue_entity' => 'Administer blue entities',
'access blue_entity overview' => 'Access the blue entities overview page',
'create blue_entity' => 'Create blue entities',
'update any blue_entity' => 'Update any blue entity',
'update own blue_entity' => 'Update own blue entities',
'delete any blue_entity' => 'Delete any blue entity',
'delete own blue_entity' => 'Delete own blue entities',
'view any blue_entity' => 'View any blue entities',
'view own blue_entity' => 'View own blue entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
// Content entity type with bundles.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('white_entity');
$entity_type->getSingularLabel()->willReturn('white entity');
$entity_type->getPluralLabel()->willReturn('white entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(FALSE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer white_entity' => 'Administer white entities',
'access white_entity overview' => 'Access the white entities overview page',
'create first white_entity' => 'First: Create white entities',
'update first white_entity' => 'First: Update white entities',
'delete first white_entity' => 'First: Delete white entities',
'create second white_entity' => 'Second: Create white entities',
'update second white_entity' => 'Second: Update white entities',
'delete second white_entity' => 'Second: Delete white entities',
'view any white_entity' => 'View any white entities',
'view any first white_entity' => 'First: View any white entities',
'view any second white_entity' => 'Second: View any white entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
// Content entity type with bundles and owner.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('black_entity');
$entity_type->getSingularLabel()->willReturn('black entity');
$entity_type->getPluralLabel()->willReturn('black entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(FALSE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer black_entity' => 'Administer black entities',
'access black_entity overview' => 'Access the black entities overview page',
'create third black_entity' => 'Third: Create black entities',
'update any third black_entity' => 'Third: Update any black entity',
'update own third black_entity' => 'Third: Update own black entities',
'delete any third black_entity' => 'Third: Delete any black entity',
'delete own third black_entity' => 'Third: Delete own black entities',
'view any black_entity' => 'View any black entities',
'view own black_entity' => 'View own black entities',
'view any third black_entity' => 'Third: View any black entities',
'view own third black_entity' => 'Third: View own black entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
// Content entity type with bundles and owner and entity published.
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->getProvider()->willReturn('entity_module_test');
$entity_type->id()->willReturn('pink_entity');
$entity_type->getSingularLabel()->willReturn('pink entity');
$entity_type->getPluralLabel()->willReturn('pink entities');
$entity_type->entityClassImplements(EntityOwnerInterface::class)->willReturn(TRUE);
$entity_type->entityClassImplements(EntityPublishedInterface::class)->willReturn(TRUE);
$entity_type->getPermissionGranularity()->willReturn('bundle');
$expected_permissions = [
'administer pink_entity' => 'Administer pink entities',
'access pink_entity overview' => 'Access the pink entities overview page',
'view own unpublished pink_entity' => 'View own unpublished pink entities',
'create third pink_entity' => 'Third: Create pink entities',
'update any third pink_entity' => 'Third: Update any pink entity',
'update own third pink_entity' => 'Third: Update own pink entities',
'delete any third pink_entity' => 'Third: Delete any pink entity',
'delete own third pink_entity' => 'Third: Delete own pink entities',
'view any pink_entity' => 'View any pink entities',
'view own pink_entity' => 'View own pink entities',
'view any third pink_entity' => 'Third: View any pink entities',
'view own third pink_entity' => 'Third: View own pink entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
return $data;
}
}