diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/.DS_Store differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 86fe66c6..f97bf32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ Changelog 20-NOV-2016 8.x-1.0-alpha6 06-DEC-2016 Adds the domain_alpha module to handle critical pre-release updates. 13-DEC-2016 8.x-1.0-alpha7 +12-MAR-2017 8.x-1.0-alpha8 +23-APR-2017 8.x-1.0-alpha9 +01-DEC-2017 8.x-1.0-alpha10 +19-DEC-2017 8.x-1.0-alpha11 +12-FEB-2018 8.x-1.0-alpha12 +07-MAR-2018 8.x-1.0-alpha13 +19-OCT-2018 8.x-1.0-alpha14 +21-FEB-2019 8.x-1.0-alpha15 +21-JUN-2019 8.x-1.0-alpha16 +19-JUN-2020 8.x-1.0-beta1 +24-JUN-2020 8.x-1.0-beta2 +16-FEB-2021 8.x-1.0-beta3 +19-FEB-2021 8.x-1.0-beta4 +25-FEB-2021 8.x-1.0-beta5 +24-JUN-2021 8.x-1.0-beta6 Status ==== @@ -64,59 +79,63 @@ marked with [x] are considered complete. - [x] Actions for domain operations - [x] Drush support for domain operations - [x] Replace / inject the storage manager in DomainAliasLoader. -- [x] Replace / inject the storage manager in DomainLoader. -- [ ] Write tests for Domain Content. +- [x] Replace / inject the storage manager in domainStorage. +- [x] Write tests for Domain Content. - [x] Views access handler for domain content. -- [ ] Restrict Domain Source options using JS -- [ ] Recreate the Domain Theme module -- [ ] Advanced drush integration / complete labelled tasks -- [ ] Check domain responses on configuration forms +- [x] Restrict Domain Source options using JS - [x] Handle site name overrides -- perhaps as a new field? - [x] Restore the `domain_grant_all` permission? - [x] Domain token support -- [ ] Test cron handling - [x] Module configurations - [x] Allow configuration of access-exempt paths for inactive domains - [x] www prefix handling - [x] Add domain-specific CSS classes - [x] Path matching for URL rewrites? - [x] Allow non-ascii characters in domains -- [ ] Recreate the Domain Nav module -- [ ] Support Tour module +- [x] Recreate the Domain Nav module - [x] Allow selective access to domain record editing - [x] Allow access to actions based on assigned domain permissions -- [ ] Implement theme functions or twig templates where proper -- [ ] Tests for all module hooks +- [x] Tests for all module hooks - [x] Proper tests for domain record validation - [x] Check test logic in testDomainAliasNegotiator() - [x] Test that sort logic in DomainAliasLoader matches what is documented -- [ ] Error handling in DomainAliasForm -- [ ] Error checking in DomainAliasController -- [ ] Deprecated methods in DomainAliasController -- [ ] Error reporting in `domain_alias_domain_request_alter()` -- [ ] Ensure completeness of DomainAccessPermissionsTest -- [ ] Check module setup behavior in tests -- [ ] Make all affiliates default value configurable? -- [ ] Cache in the DomainAccessManager -- [ ] Remove deprecated `entity_get_form_display` -- [ ] Review drupalUserIsLoggedIn() hack +- [x] Error handling in DomainAliasForm +- [x] Error checking in DomainAliasController +- [x] Deprecated methods in DomainAliasController +- [x] Error reporting in `domain_alias_domain_request_alter()` +- [x] Ensure completeness of DomainAccessPermissionsTest +- [x] Check module setup behavior in tests +- [x] Make all affiliates default value configurable +- [x] Review drupalUserIsLoggedIn() hack - [x] Review DomainNegotiatorTest for completeness - [x] Review core note in DomainEntityReferenceTest -- [ ] Expand DomainActionsTest -- [ ] DomainViewBuilder review -- [ ] Dependency Injection in DomainValidator -- [ ] Caching strategies in DomainNegotiator -- [ ] Caching strategies in DomainConfigOverrides +- [x] Expand DomainActionsTest +- [x] DomainViewBuilder review +- [x] Dependency Injection in DomainValidator - [x] Inject the module handler service in DomainListBuilder::getOperations() -- [ ] `drush_domain_generate_domains()` has odd counting logic +- [x] `drush_domain_generate_domains()` has odd counting logic - [x] Separate permissions for Domain Alias -- [ ] Check loader logic in the DomainSource PathProcessor -- [ ] Check loader logic in Domain Access node_access -- [ ] Check id logic in Domain Alias list controller +- [x] Check loader logic in the DomainSource PathProcessor +- [x] Check loader logic in Domain Access `node_access` +- [x] Check id logic in Domain Alias list controller +- [x] Check domain responses on configuration forms +- [x] Remove deprecated `entity_get_form_display` +- [x] Implement theme functions or twig templates where proper +- [x] Advanced drush integration / complete labelled tasks +- [ ] Add filter options to domain_access and domain_source views +- [ ] Test cron handling +- [ ] Caching strategies in DomainNegotiator +- [ ] Caching strategies in DomainConfigOverrides +- [ ] Cache in the DomainAccessManager +- [ ] Proper handling of default node values +- [ ] Do not allow actions to be edited? +- [o] Recreate the Domain Theme module -- see https://www.drupal.org/project/domain_theme_switch # Final - [ ] Security review - [ ] Provide an upgrade path from 6.x - [ ] Provide an upgrade path from 7.x-3.x -- [ ] Remove calls to deprecated methods / classes +- [x] Remove calls to deprecated methods / classes - [ ] Remove unnecessary use statements +- [ ] Support Tour module +- [ ] Views schema fails -- see https://www.drupal.org/project/drupal/issues/2834801 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/LICENSE.txt @@ -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. + + + Copyright (C) + + 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. + + , 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. diff --git a/README.md b/README.md index e055e33b..691d60bb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ Domain ====== -Domain module for Drupal port to Drupal 8. +The Domain module suite lets you share users, content, and configuration across a group of domains from a single installation and database. -Active branch is 8-x.1-x. Begin any forks from there. +Current Status +------ + +Domain module for Drupal port to Drupal 8, under active development. + +Domain required Drupal 8.5 or higher. + +Active branch is the 8-x.1-x branch in GitHub. Begin any forks from there. The underlying API is stable, and it's currently usable for access control. The configuration supports manual editing. Themes should work. Views and Bulk @@ -11,16 +18,40 @@ Operations are not yet supported. For a complete feature status list, see [CHANGELOG.md](https://github.com/agentrickard/domain/blob/8.x-1.x/CHANGELOG.md) -Alpha release updates ------- +Included modules +------- + +* *Domain* + The core module. Domain provides means for registering multiple domains within a + single Drupal installation. It allows users to be assigned as domain administrators, + provides a Block and Views display context, and creates a default entity reference + field for use by other modules. + +* *Domain Access* + Provides node access controls based on domains. (This module contains much of the + Drupal 7 functionality). It allows users to be assigned as editors of content per-domain, + sets content visibility rules, and provides Views integration for content. + +* *Domain Alias* + Allows multiple hostnames to be pointed to a single registered domain. These aliases + can include wildcards (such as *.example.com) and may be configured to redirect to + their canonical domain. Domain Alias also allows developers to register aliases per + `environment`, so that different hosts are used consistently across development + environments. See the README file for Domain Alias for more information. -A limited set of updates are provided for major architecture changes during the -alpha release. +* *Domain Config* + Provides a means for changing configuration settings on a per-domain basis. See the + README for Domain Config for more information. -You can run these updates by enabling the `domain_alpha` module and running -Drupal's database updates. The updates and affected versions are: +* *Domain Content* + Provides content overview pages on a per-domain basis, so that editors may review + content assigned to specific domains. This module is a series of Views. + +* *Domain Source* + Allows content to be assigned a canonical domain when writing URLs. Domain Source will + ensure that content that appears on multiple domains always links to one URL. See + the module's README for more information. -* Update 8001: Affects versions of Alpha6 or lower. Implementation Notes ====== @@ -34,10 +65,53 @@ To use cross-domain logins, you must now set the *cookie_domain* value in To do so, clone `default.services.yml` to `services.yml` and change the `cookie_domain` value to match the root hostname of your sites. Note that cross-domain login requires the sharing of a top-level domain, so a setting like -`*.example.com` will work for all `example.com` subdomains. +`.example.com` will work for all `example.com` subdomains. + +Example: + +``` +cookie_domain: '.example.com' +``` See https://www.drupal.org/node/2391871. +Cross-Site HTTP requests (CORS) +------ +As of Drupal 8.2, it's possible to allow a particular site to enable CORS for responses +served by Drupal. + +In the case of Domain, allowing CORS may remove AJAX errors caused when using some forms, +particularly entity references, when the AJAX request goes to another domain. + +This feature is not enabled by default because there are security consequences. See +https://www.drupal.org/node/2715637 for more information and instructions. + +To enable CORS for all domains, copy `default.services.yml` to `services.yml` and enable +the following lines: + +``` + # Configure Cross-Site HTTP requests (CORS). + # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + # for more information about the topic in general. + # Note: By default the configuration is disabled. + cors.config: + enabled: false + # Specify allowed headers, like 'x-allowed-header'. + allowedHeaders: [] + # Specify allowed request methods, specify ['*'] to allow all possible ones. + allowedMethods: [] + # Configure requests allowed from specific origins. + allowedOrigins: ['*'] + # Sets the Access-Control-Expose-Headers header. + exposedHeaders: false + # Sets the Access-Control-Max-Age header. + maxAge: false + # Sets the Access-Control-Allow-Credentials header. + supportsCredentials: false +``` + +The key here is setting `enabled` to `false`. + Trusted host settings ------ @@ -46,14 +120,49 @@ and alias to the pattern list. For example: ``` $settings['trusted_host_patterns'] = array( - '^*\.example\.com$', + '^.+\.example\.org$', '^myexample\.com$', + '^myexample\.dev$', '^localhost$', ); ``` +We *strongly encourage* the use of trusted host settings. When Domain issues a redirect, +it will check the domain hostname against these settings. Any redirect that does not +match the trusted host settings will be denied and throw an error. + See https://www.drupal.org/node/1992030 for more information. +Configuring domain records +------- +To create a domain record, you must provide the following information: + +* A unique *hostname*, which may include a port. (Therefore, example.com and +example.com:8080 are considered different.) The hostname may only contain +alphanumeric characters, dashes, dots, and one colon. If you wish to use +international domain names, toggle the `Allow non-ASCII characters in domains + and aliases.` setting. +* A *machine_name* that must be unique. This value will be autogenerated and +cannot be edited once created. +* A *name* to be used in lists of domains. +* A URL scheme, used for writing links to the domain. The scheme may be `http`, +`https`, or `variable`. If `variable` is used, the scheme will be inherited from +the server or request settings. This option is good if your test environments +do not have secure certificates but your production environment does. +* A *status* indicating `active` or `inactive`. Inactive domains may only be +viewed by users with permission to `view inactive domains` all other users will +be redirected to the default domain (see below). +* The *weight* to be used when sorting domains. These values autoincrement as +new domains are created. +* Whether the domain is the *default* or not. Only one domain can be set as + `default`. The default domain is used for redirects in cases where other + domains are either restricted (inactive) or fail to load. This value can be + reassigned after domains are created. + +Domain records are *configuration entities*, which means they are not stored in +the database nor accessible to Views by default. They are, however, exportable +as part of your configuration. + Domains and caching ------ @@ -70,6 +179,8 @@ already done so) and change the `required_cache_contexts` value to: The addition of `url.site` should provide the domain context that the cache layer requires. +For developers, see also the information in the Domain Alias README. + Contributing ==== @@ -88,23 +199,16 @@ a beta release on Drupal.org. We would like to tackle issues in that order, but feel free to work on what motivates you. -Testing +Testing [![Build Status](https://travis-ci.com/agentrickard/domain.svg?branch=8.x-1.x)](https://travis-ci.com/agentrickard/domain) ==== @zerolab built a Travis definition file for automated testing! That means all pull requests will automatically run tests! -[![Build Status](https://travis-ci.org/agentrickard/domain.svg?branch=8.x-1.x)](https://travis-ci.org/agentrickard/domain) - -The module does have solid test coverage, and complete coverage is required for release. -Right now, we mostly use SimpleTest, because it is most familiar, and much of our -testing is about browser and http behavior. - If you file a pull request or patch, please (at a minimum) run the existing tests to check for failures. Writing additional tests will greatly speed completion, as I won't commit code without test coverage. -I use SimpleTest, though unit tests would also be welcome -- as would kernel tests. Those -might take longer to review. +New tests should be written in PHPUnit as Functional, Kernel, or Unit tests. Because Domain requires varying http host requests to test, we can't normally use the Drupal.org testing infrastructure. (This may change, but we are not counting on it.) @@ -116,6 +220,3 @@ point to your drupal instance. I use variants of `example.com` for local tests. in most test cases. See `DomainTestBase::domainCreateTestDomains()` for the logic. When running tests, you normally need to be on the default domain. - -If anyone is capable of building a vagrant box to simplify testing, that would be ideal. - diff --git a/define_subdomains.sh b/define_subdomains.sh new file mode 100644 index 00000000..4b752ca9 --- /dev/null +++ b/define_subdomains.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +declare -a HOSTS=(${CONTAINER_NAME} 'example.com') +declare -a SUBDOMAINS=('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten') + +for HOST in ${HOSTS[@]}; do + echo '127.0.0.1' ${HOST} >> /etc/hosts + + for SUBDOMAIN in ${SUBDOMAINS[@]}; do + echo '127.0.0.1' ${SUBDOMAIN}.${HOST} >> /etc/hosts + echo '127.0.0.1' ${SUBDOMAIN}.example.${HOST} >> /etc/hosts + done +done diff --git a/domain/config/install/domain.settings.yml b/domain/config/install/domain.settings.yml index 946b224d..bdcce533 100644 --- a/domain/config/install/domain.settings.yml +++ b/domain/config/install/domain.settings.yml @@ -1,4 +1,4 @@ allow_non_ascii: false www_prefix: false -login_paths: '/user/login\r\n/user/password' +login_paths: "/user/login\r\n/user/password" css_classes: '' diff --git a/domain/config/install/field.field.user.user.field_domain_admin.yml b/domain/config/install/field.field.user.user.field_domain_admin.yml index 4671135e..e70c002b 100644 --- a/domain/config/install/field.field.user.user.field_domain_admin.yml +++ b/domain/config/install/field.field.user.user.field_domain_admin.yml @@ -17,9 +17,8 @@ default_value: { } default_value_callback: '' settings: handler: 'domain:domain' - handler_settings: - target_bundles: null - sort: - field: weight - direction: ASC + target_bundles: null + sort: + field: weight + direction: ASC field_type: entity_reference diff --git a/domain/config/schema/domain.schema.yml b/domain/config/schema/domain.schema.yml index 3434953c..aa0138d7 100644 --- a/domain/config/schema/domain.schema.yml +++ b/domain/config/schema/domain.schema.yml @@ -61,6 +61,20 @@ action.configuration.domain_enable_action: type: action_configuration_default label: 'Enable domain record' +block.settings.domain_nav_block: + type: block_settings + label: 'Domain navigation block' + mapping: + link_options: + type: string + label: 'Link paths' + link_theme: + type: string + label: 'Link theme' + link_label: + type: string + label: 'Link text' + condition.plugin.domain: type: condition.plugin mapping: @@ -68,3 +82,14 @@ condition.plugin.domain: type: sequence sequence: type: string + +views.access.domain: + type: mapping + label: 'Domains' + mapping: + domain: + type: sequence + label: 'List of domains' + sequence: + type: string + label: 'Domain' diff --git a/domain/domain.api.php b/domain/domain.api.php index a82160fc..4b7a9552 100644 --- a/domain/domain.api.php +++ b/domain/domain.api.php @@ -12,7 +12,7 @@ * * use Drupal\domain\DomainInterface; * - * @param array $domains + * @param \Drupal\domain\DomainInterface[] $domains * An array of $domain record objects. */ function hook_domain_load(array $domains) { @@ -40,7 +40,7 @@ function hook_domain_load(array $domains) { */ function hook_domain_request_alter(\Drupal\domain\DomainInterface &$domain) { // Add a special case to the example domain. - if ($domain->getMatchType() == \Drupal\domain\DomainNegotiator::DOMAIN_MATCH_EXACT && $domain->id() == 'example_com') { + if ($domain->getMatchType() == \Drupal\domain\DomainNegotiatorInterface::DOMAIN_MATCHED_EXACT && $domain->id() == 'example_com') { // Do something here. $domain->addProperty('foo', 'Bar'); } @@ -70,11 +70,11 @@ function hook_domain_operations(\Drupal\domain\DomainInterface $domain, \Drupal\ if ($account->hasPermission('view domain aliases') || $account->hasPermission('administer domain aliases')) { // Add aliases to the list. $id = $domain->id(); - $operations['domain_alias'] = array( + $operations['domain_alias'] = [ 'title' => t('Aliases'), - 'url' => Url::fromRoute('domain_alias.admin', array('domain' => $id)), + 'url' => \Drupal\Core\Url::fromRoute('domain_alias.admin', ['domain' => $id]), 'weight' => 60, - ); + ]; } return $operations; } @@ -88,6 +88,9 @@ function hook_domain_operations(\Drupal\domain\DomainInterface $domain, \Drupal\ * * NOTE: This does not apply to Domain Alias records. * + * No return value. Modify $error_list by reference. Return an empty array + * or NULL to validate this domain. + * * @param array &$error_list * The list of current validation errors. Modify this value by reference. * If you return an empty array or NULL, the domain is considered valid. @@ -96,12 +99,9 @@ function hook_domain_operations(\Drupal\domain\DomainInterface $domain, \Drupal\ * Note that this is checked for uniqueness separately. This value is not * modifiable. * - * No return value. Modify $error_list by reference. Return an empty array - * or NULL to validate this domain. - * * @see domain_valid_domain() */ -function hook_domain_validate_alter(&$error_list, $hostname) { +function hook_domain_validate_alter(array &$error_list, $hostname) { // Only allow TLDs to be .org for our site. if (substr($hostname, -4) != '.org') { $error_list[] = t('Only .org domains may be registered.'); @@ -114,6 +114,8 @@ function hook_domain_validate_alter(&$error_list, $hostname) { * Note that this hook does not fire for users with the 'administer domains' * permission. * + * No return value. Modify the $query object via methods. + * * @param \Drupal\Core\Entity\Query\QueryInterface $query * An entity query prepared by DomainSelection::buildEntityQuery(). * @param \Drupal\Core\Session\AccountInterface $account @@ -126,13 +128,11 @@ function hook_domain_validate_alter(&$error_list, $hostname) { * 'editor' for assigning editorial permissions (as in Domain Access) * 'admin' for assigning administrative permissions for a specific domain. * Most contributed modules will use 'editor'. - * - * No return value. Modify the $query object via methods. */ -function hook_domain_references_alter($query, $account, $context) { +function hook_domain_references_alter(\Drupal\Core\Entity\Query\QueryInterface $query, \Drupal\Core\Session\AccountInterface $account, array $context) { // Remove the default domain from non-admins when editing nodes. if ($context['entity_type'] == 'node' && $context['field_type'] == 'editor' && !$account->hasPermission('edit assigned domains')) { - $default = \Drupal::service('domain.loader')->loadDefaultId(); + $default = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultId(); $query->condition('id', $default, '<>'); } } diff --git a/domain/domain.drush.inc b/domain/domain.drush.inc index 4ff28870..353ae971 100644 --- a/domain/domain.drush.inc +++ b/domain/domain.drush.inc @@ -7,149 +7,152 @@ use Drupal\Component\Utility\Html; use Drupal\domain\DomainInterface; +use GuzzleHttp\Exception\RequestException; /** * Implements hook_drush_command(). */ function domain_drush_command() { - $items = array(); + $items = []; - $items['domain-list'] = array( + $items['domain-list'] = [ 'description' => 'List active domains for the site.', - 'examples' => array( + 'examples' => [ 'drush domain-list', 'drush domains', - ), - 'aliases' => array('domains'), - ); - $items['domain-add'] = array( + ], + 'aliases' => ['domains'], + ]; + $items['domain-add'] = [ 'description' => 'Add a new domain to the site.', - 'examples' => array( + 'examples' => [ 'drush domain-add example.com \'My Test Site\'', 'drush domain-add example.com \'My Test Site\' --inactive=1 --https==1', 'drush domain-add example.com \'My Test Site\' --weight=10', - ), - 'arguments' => array( + 'drush domain-add example.com \'My Test Site\' --validate=1', + ], + 'arguments' => [ 'hostname' => 'The domain hostname to register (e.g. example.com).', 'name' => 'The name of the site (e.g. Domain Two).', - ), - 'options' => array( + ], + 'options' => [ 'inactive' => 'Set the domain to inactive status if set.', 'https' => 'Use https protocol for this domain if set.', 'weight' => 'Set the order (weight) of the domain.', - 'is_default' => 'Set this domain as the default domain.' - ), - ); - $items['domain-delete'] = array( + 'is_default' => 'Set this domain as the default domain.', + 'validate' => 'Force a check of the URL response before allowing registration.', + ], + ]; + $items['domain-delete'] = [ 'description' => 'Delete a domain from the site.', - 'examples' => array( + 'examples' => [ 'drush domain-delete example.com', 'drush domain-delete 1', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain' => 'The numeric id or hostname of the domain to delete.', - ), - ); - $items['domain-test'] = array( + ], + ]; + $items['domain-test'] = [ 'description' => 'Tests domains for proper response. If run from a subfolder, you must specify the --uri.', - 'examples' => array( + 'examples' => [ 'drush domain-test', 'drush domain-test example.com', 'drush domain-test 1', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to test. If no value is passed, all domains are tested.', - ), - 'options' => array( + ], + 'options' => [ 'base_path' => 'The subdirectory name if Drupal is installed in a folder other than server root.', - ), - ); - $items['domain-default'] = array( + ], + ]; + $items['domain-default'] = [ 'description' => 'Sets the default domain. If run from a subfolder, you must specify the --uri.', - 'examples' => array( + 'examples' => [ 'drush domain-default example.com', 'drush domain-default 1', - 'drush domain-default 1 --skip_check=1', - ), - 'arguments' => array( + 'drush domain-default 1 --validate=1', + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to make default.', - ), - 'options' => array( - 'skip_check' => 'Bypass the domain response test.' - ), - ); - $items['domain-disable'] = array( + ], + 'options' => [ + 'validate' => 'Force a check of the URL response before allowing registration.', + ], + ]; + $items['domain-disable'] = [ 'description' => 'Sets a domain status to off.', - 'examples' => array( + 'examples' => [ 'drush domain-disable example.com', 'drush domain-disable 1', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to disable.', - ), - ); - $items['domain-enable'] = array( + ], + ]; + $items['domain-enable'] = [ 'description' => 'Sets a domain status to on.', - 'examples' => array( + 'examples' => [ 'drush domain-disable example.com', 'drush domain-disable 1', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to enable.', - ), - ); - $items['domain-name'] = array( + ], + ]; + $items['domain-name'] = [ 'description' => 'Changes a domain label.', - 'examples' => array( + 'examples' => [ 'drush domain-name example.com Foo', 'drush domain-name 1 Foo', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to relabel.', 'name' => 'The name to use for the domain.', - ), - ); - $items['domain-machine-name'] = array( + ], + ]; + $items['domain-machine-name'] = [ 'description' => 'Changes a domain name.', - 'examples' => array( + 'examples' => [ 'drush domain-machine-name example.com foo', 'drush domain-machine-name 1 foo', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to rename.', 'name' => 'The machine-readable name to use for the domain.', - ), - ); - $items['domain-scheme'] = array( + ], + ]; + $items['domain-scheme'] = [ 'description' => 'Changes a domain scheme.', - 'examples' => array( + 'examples' => [ 'drush domain-scheme example.com https', 'drush domain-scheme 1 https', - ), - 'arguments' => array( + ], + 'arguments' => [ 'domain_id' => 'The numeric id or hostname of the domain to change.', 'scheme' => 'The URL schema (http or https) to use for the domain.', - ), - ); - $items['generate-domains'] = array( + ], + ]; + $items['generate-domains'] = [ 'description' => 'Generate domains for testing.', - 'arguments' => array( + 'arguments' => [ 'primary' => 'The primary domain to use. This will be created and used for *.example.com hostnames.', - ), - 'options' => array( + ], + 'options' => [ 'count' => 'The count of extra domains to generate. Default is 15.', - 'empty' => 'Pass empty=1 to truncate the {domain} table before creating records.' - ), - 'examples' => array( + 'empty' => 'Pass empty=1 to truncate the {domain} table before creating records.', + ], + 'examples' => [ 'drush domain-generate example.com', 'drush domain-generate example.com --count=25', 'drush domain-generate example.com --count=25 --empty=1', 'drush gend', 'drush gend --count=25', 'drush gend --count=25 --empty=1', - ), - 'aliases' => array('gend'), - ); + ], + 'aliases' => ['gend'], + ]; return $items; } @@ -168,14 +171,14 @@ function domain_drush_help($section) { * Shows the domain list. */ function drush_domain_list() { - $domains = \Drupal::service('domain.loader')->loadMultipleSorted(NULL, TRUE); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultipleSorted(NULL, TRUE); if (empty($domains)) { drush_print(dt('No domains have been created. Use drush domain-add to create one.')); return; } - $header = array( + $header = [ 'weight' => dt('Weight'), 'name' => dt('Name'), 'hostname' => dt('Hostname'), @@ -184,10 +187,10 @@ function drush_domain_list() { 'is_default' => dt('Default'), 'domain_id' => dt('Domain Id'), 'id' => dt('Machine name'), - ); - $rows = array(array_values($header)); + ]; + $rows = [array_values($header)]; foreach ($domains as $domain) { - $row = array(); + $row = []; foreach ($header as $key => $name) { $row[] = Html::escape($domain->get($key)); } @@ -207,28 +210,25 @@ function drush_domain_list() { * The script may also add test1, test2, test3 up to any number to test a * large number of domains. This test is mostly for UI testing. * - * @param $primary + * @param string $primary * The root domain to use for domain creation. - * - * @return - * A list of the domains created. */ function drush_domain_generate_domains($primary = 'example.com') { // Check the number of domains to create. $count = drush_get_option('count'); - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); if (empty($count)) { $count = 15; } // Ensure we don't duplicate any domains. - $existing = array(); + $existing = []; if (!empty($domains)) { foreach ($domains as $domain) { $existing[] = $domain->getHostname(); } } // Set up one.* and so on. - $names = array( + $names = [ 'one', 'two', 'three', @@ -242,46 +242,45 @@ function drush_domain_generate_domains($primary = 'example.com') { 'foo', 'bar', 'baz', - ); + ]; // Set the creation array. - $new = array($primary); + $new = [$primary]; foreach ($names as $name) { $new[] = $name . '.' . $primary; } // Include a non hostname. $new[] = 'my' . $primary; // Filter against existing so we can count correctly. - $prepared = array(); + $prepared = []; foreach ($new as $key => $value) { if (!in_array($value, $existing)) { $prepared[] = $value; } } - // Add any test domains. - if ($count > 15 || empty($prepared)) { - // @TODO fix this logic. - $start = 1; - $j = count($prepared); - for ($i = $start + 1; $j <= $count; $i++) { - $prepared[] = 'test' . $i . '.' . $primary; - $j++; - } + // Add any test domains that have numeric prefixes. We don't expect these URLs + // to work, and mainly use these for testing the user interface. + $needed = $count - count($prepared); + for ($i = 1; $i <= $needed; $i++) { + $prepared[] = 'test' . $i . '.' . $primary; } + // Get the initial item weight for sorting. $start_weight = count($domains); $prepared = array_slice($prepared, 0, $count); + + // Create the domains. foreach ($prepared as $key => $item) { - $hostname = strtolower($item); - $values = array( + $hostname = mb_strtolower($item); + $values = [ 'name' => ($item != $primary) ? ucwords(str_replace(".$primary", '', $item)) : \Drupal::config('system.site')->get('name'), 'hostname' => $hostname, 'scheme' => 'http', 'status' => 1, 'weight' => ($item != $primary) ? $key + $start_weight + 1 : -1, 'is_default' => 0, - 'id' => \Drupal::service('domain.creator')->createMachineName($hostname), - ); - $domain = \Drupal::service('domain.creator')->createDomain($values); + 'id' => \Drupal::entityTypeManager()->getStorage('domain')->createMachineName($hostname), + ]; + $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); domain_drush_create($domain); } @@ -294,67 +293,65 @@ function drush_domain_generate_domains($primary = 'example.com') { /** * Validates the domain generation script. * - * @param $primary + * @param string $primary * The root domain to use for domain creation. */ function drush_domain_generate_domains_validate($primary = 'example.com') { if ($empty = drush_get_option('empty')) { - db_query("TRUNCATE TABLE {domain}"); + \Drupal::database()->truncate("domain")->execute(); } return; - // TODO: Update this validation. - $error = domain_valid_domain($primary); - if (!empty($error)) { - return drush_set_error('domain', $error); - } + // TODO: Add validation. } /** * Adds a new domain. * - * @param $hostname + * @param string $hostname * The domain name to register. - * @param $name + * @param string $name * The name to use for this domain. - * - * @return - * The domain created or an error message. */ function drush_domain_add($hostname, $name) { - $start_weight = count(\Drupal::service('domain.loader')->loadMultiple()) + 1; - $hostname = strtolower($hostname); - $values = array( + $records_count = \Drupal::entityTypeManager()->getStorage('domain')->getQuery()->count()->execute(); + $start_weight = $records_count + 1; + $hostname = mb_strtolower($hostname); + /** @var \Drupal\domain\DomainStorageInterface $domain_storage */ + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $values = [ 'hostname' => $hostname, 'name' => $name, 'status' => (!drush_get_option('invalid')) ? 1 : 0, 'scheme' => (!drush_get_option('https')) ? 'http' : 'https', 'weight' => ($weight = drush_get_option('weight')) ? $weight : $start_weight + 1, 'is_default' => ($is_default = drush_get_option('is_default')) ? $is_default : 0, - 'id' => \Drupal::service('domain.creator')->createMachineName($hostname), - ); - $domain = \Drupal::service('domain.creator')->createDomain($values); - /* TODO: Fix this check. - if (!empty($domain->is_default)) { - $domain->checkResponse(); - drush_print($domain->response); - } - */ + 'id' => $domain_storage->createMachineName($hostname), + 'validate_url' => (drush_get_option('validate')) ? 1 : 0, + ]; + $domain = $domain_storage->create($values); domain_drush_create($domain); } /** - * Validates the domain add script. + * Validates the domain add command arguments. * - * @param $hostname + * @param string $hostname * The domain name to register. - * @param $name + * @param string $name * The name to use for this domain. + * + * @return bool + * TRUE when validation passed, FALSE otherwise. */ function drush_domain_add_validate($hostname, $name) { - $error = domain_drush_validate_domain($hostname); - if (!empty($error)) { - return drush_set_error('domain', $error); + $errors = domain_drush_validate_domain($hostname); + if (!empty($errors)) { + return drush_set_error('domain', $errors); + } + elseif (\Drupal::entityTypeManager()->getStorage('domain')->loadByHostname($hostname)) { + return drush_set_error('domain', dt('The hostname is already registered.')); } + return TRUE; } /** @@ -364,41 +361,73 @@ function drush_domain_add_validate($hostname, $name) { * A domain entity. */ function domain_drush_create(DomainInterface $domain) { - $domain->save(); - if ($domain->getDomainId()) { - drush_print(dt('Created @name at @domain.', array('@name' => $domain->label(), '@domain' => $domain->getHostname()))); + if ($error = domain_drush_check_response($domain)) { + drush_set_error('hostname', $error); } else { - drush_print(dt('The request could not be completed.')); + $domain->save(); + if ($domain->getDomainId()) { + drush_print(dt('Created @name at @domain.', ['@name' => $domain->label(), '@domain' => $domain->getHostname()])); + } + else { + drush_print(dt('The request could not be completed.')); + } + } +} + +/** + * Runs a check to ensure that the domain is responsive. + * + * @param Drupal\domain\DomainInterface $domain + * A domain entity. + * + * @return string + * An error message if the domain url does not validate. Else empty. + */ +function domain_drush_check_response(DomainInterface $domain) { + // Check the domain response. First, clear the path value. + if ($domain->validate_url) { + $domain->setPath(); + try { + $response = $domain->getResponse(); + } + // We cannot know which Guzzle Exception class will be returned; be generic. + catch (RequestException $e) { + watchdog_exception('domain', $e); + // File a general server failure. + $domain->setResponse(500); + } + // If validate_url is set, then we must receive a 200 response. + if ($domain->getResponse() != 200) { + if (empty($response)) { + $response = 500; + } + return dt('The server request to @url returned a @response response. To proceed, disable the test of the server response by leaving off the --validate flag.', ['@url' => $domain->getPath(), '@response' => $response]); + } } } /** * Validates a domain. * - * @param $hostname + * @param string $hostname * The domain name to validate for syntax and uniqueness. * - * @return + * @return array * An array of errors encountered. * * @see domain_validate() */ function domain_drush_validate_domain($hostname) { - return; - // TODO: Restore this validation. - $error = domain_validate($hostname); - $output = ''; - foreach ($error as $msg) { - $output .= $msg; - } - return $output; + /** @var \Drupal\domain\DomainValidatorInterface $validator */ + $validator = \Drupal::service('domain.validator'); + return $validator->validate($hostname); } /** * Deletes a domain record. * - * @param $argument + * @param string $argument * The domain_id to delete. Pass 'all' to delete all records. */ function drush_domain_delete($argument = NULL) { @@ -406,12 +435,12 @@ function drush_domain_delete($argument = NULL) { drush_set_error('domain', dt('You must specify a domain to delete.')); } if ($argument == 'all') { - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); if (empty($domains)) { drush_print(dt('There are no domains to delete.')); return; } - $content = drush_choice(array(1 => dt('Delete all domains')), dt('This action may not be undone. Continue?'), '!value'); + $content = drush_choice([1 => dt('Delete all domains')], dt('This action may not be undone. Continue?'), '!value'); if (empty($content)) { return; } @@ -428,14 +457,14 @@ function drush_domain_delete($argument = NULL) { } foreach ($domains as $domain) { $domain->delete(); - drush_print(dt('Domain record @domain deleted.', array('@domain' => $domain->getHostname()))); + drush_print(dt('Domain record @domain deleted.', ['@domain' => $domain->getHostname()])); } return; // TODO: Set options for re-assigning content. - $list = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - $options = array('0' => dt('Do not reassign')); + $list = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + $options = ['0' => dt('Do not reassign')]; foreach ($list as $data) { if ($data->id() != $domain->id()) { $options[$data->getDomainId()] = $data->getHostname(); @@ -460,7 +489,7 @@ function drush_domain_delete($argument = NULL) { /** * Tests a domain record for the proper HTTP response. * - * @param $argument + * @param string $argument * The domain_id to test. Passing no value tests all records. */ function drush_domain_test($argument = NULL) { @@ -469,11 +498,11 @@ function drush_domain_test($argument = NULL) { $GLOBALS['base_path'] = '/' . $base_path . '/'; } if (is_null($argument)) { - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); } else { if ($domain = drush_domain_get_from_argument($argument)) { - $domains = array($domain); + $domains = [$domain]; } else { return; @@ -481,10 +510,10 @@ function drush_domain_test($argument = NULL) { } foreach ($domains as $domain) { if ($domain->getResponse() != 200) { - drush_print(dt('Fail: !error. Please pass a --uri parameter or a --base_path to retest.' , array('!error' => $domain->getResponse()))); + drush_print(dt('Fail: !error. Please pass a --uri parameter or a --base_path to retest.', ['!error' => $domain->getResponse()])); } else { - drush_print(dt('Success: !url tested successfully.', array('!url' => $domain->getPath()))); + drush_print(dt('Success: !url tested successfully.', ['!url' => $domain->getPath()])); } } } @@ -495,19 +524,15 @@ function drush_domain_test($argument = NULL) { function drush_domain_default($argument) { // Resolve the domain. if ($domain = drush_domain_get_from_argument($argument)) { - // TODO: Check for domain response. - /* - $check = drush_get_option('skip_check'); - if (empty($check)) { - $error = domain_check_response($domain, TRUE); - if ($error) { - drush_set_error($error); - drush_print(dt('You may disable this error by passing --skip_check=1.')); - return; - } - }*/ - $domain->saveDefault(); - drush_print(dt('!domain set to primary domain.', array('!domain' => $domain->getHostname()))); + $validate = (drush_get_option('validate')) ? 1 : 0; + $domain->addProperty('validate_url', $validate); + if ($error = domain_drush_check_response($domain)) { + drush_set_error('domain', $error); + } + else { + $domain->saveDefault(); + drush_print(dt('!domain set to primary domain.', ['!domain' => $domain->getHostname()])); + } } } @@ -519,10 +544,10 @@ function drush_domain_disable($argument) { if ($domain = drush_domain_get_from_argument($argument)) { if ($domain->status()) { $domain->disable(); - drush_print(dt('!domain has been disabled.', array('!domain' => $domain->getHostname()))); + drush_print(dt('!domain has been disabled.', ['!domain' => $domain->getHostname()])); } else { - drush_print(dt('!domain is already disabled.', array('!domain' => $domain->getHostname()))); + drush_print(dt('!domain is already disabled.', ['!domain' => $domain->getHostname()])); } } } @@ -535,10 +560,10 @@ function drush_domain_enable($argument) { if ($domain = drush_domain_get_from_argument($argument)) { if (!$domain->status()) { $domain->enable(); - drush_print(dt('!domain has been enabled.', array('!domain' => $domain->getHostname()))); + drush_print(dt('!domain has been enabled.', ['!domain' => $domain->getHostname()])); } else { - drush_print(dt('!domain is already enabled.', array('!domain' => $domain->getHostname()))); + drush_print(dt('!domain is already enabled.', ['!domain' => $domain->getHostname()])); } } } @@ -557,15 +582,15 @@ function drush_domain_name($argument, $name) { * Changes a domain machine_name. */ function drush_domain_machine_name($argument, $machine_name) { - $machine_name = \Drupal::service('domain.creator')->createMachineName($machine_name); + $machine_name = \Drupal::entityTypeManager()->getStorage('domain')->createMachineName($machine_name); // Resolve the domain. if ($domain = drush_domain_get_from_argument($argument)) { $results = \Drupal::entityTypeManager() ->getStorage('domain') - ->loadByProperties(array('machine_name' => $machine_name)); + ->loadByProperties(['machine_name' => $machine_name]); foreach ($results as $result) { if ($result->id() == $machine_name) { - drush_print(dt('The machine_name @machine_name is being used by domain @hostname.', array('@machine_name' => $machine_name, '@hostname' => $result->getHostname()))); + drush_print(dt('The machine_name @machine_name is being used by domain @hostname.', ['@machine_name' => $machine_name, '@hostname' => $result->getHostname()])); return; } } @@ -579,7 +604,7 @@ function drush_domain_machine_name($argument, $machine_name) { function drush_domain_scheme($argument) { // Resolve the domain. if ($domain = drush_domain_get_from_argument($argument)) { - $content = drush_choice(array(1 => dt('http'), 2 => dt('https')), dt('Select the default http scheme:'), '!value'); + $content = drush_choice([1 => dt('http'), 2 => dt('https')], dt('Select the default http scheme:'), '!value'); if (empty($content)) { return; } @@ -597,9 +622,9 @@ function drush_domain_scheme($argument) { * On failure, throws a drush error. */ function drush_domain_get_from_argument($argument) { - $domain = \Drupal::service('domain.loader')->load($argument); + $domain = \Drupal::entityTypeManager()->getStorage('domain')->load($argument); if (!$domain) { - $domain = \Drupal::service('domain.loader')->loadByHostname($argument); + $domain = \Drupal::entityTypeManager()->getStorage('domain')->loadByHostname($argument); } if (!$domain) { drush_set_error('domain', dt('Domain record not found.')); diff --git a/domain/domain.info.yml b/domain/domain.info.yml index 3270a0aa..0ab968a3 100644 --- a/domain/domain.info.yml +++ b/domain/domain.info.yml @@ -2,8 +2,13 @@ name: Domain description: 'Creates domain records within a Drupal installation.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - options + - drupal:options configure: domain.admin + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain/domain.install b/domain/domain.install index 64f1d365..4939d3a2 100644 --- a/domain/domain.install +++ b/domain/domain.install @@ -1,18 +1,33 @@ get('purge_batch_size'); + field_purge_batch($limit); + \Drupal::entityTypeManager()->clearCachedDefinitions(); } /** @@ -21,20 +36,56 @@ function domain_install() { * Installs the domain admin field on users. */ function domain_update_8001() { - domain_confirm_fields('user', 'user', 'domain:domain'); + _domain_configure_field(); } /** - * Implements hook_uninstall(). - * - * Removes domain admin field on uninstall. + * Configures user form display to checkboxes widget for domain admin field. */ -function domain_uninstall() { - $field_storage_config = \Drupal::entityTypeManager() - ->getStorage('field_storage_config'); +function _domain_configure_field() { + if ($display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load('user.user.default')) { + $display->setComponent(DomainInterface::DOMAIN_ADMIN_FIELD, [ + 'type' => 'options_buttons', + 'weight' => 50, + ])->save(); + } +} - $id = $type . '.' . DOMAIN_ADMIN_FIELD; - if ($field = $field_storage_config->load($id)) { - $field->delete(); +/** + * Updates block domain context_mapping for Drupal 8.8 and higher. + */ +function domain_update_8002() { + $new_context_id = '@domain.current_domain_context:domain'; + $config_factory = \Drupal::configFactory(); + $update_list = []; + foreach ($config_factory->listAll('block.block.') as $block_config_name) { + $update_block = FALSE; + $block = $config_factory->getEditable($block_config_name); + if ($visibility = $block->get('visibility')) { + foreach ($visibility as $condition_plugin_id => &$condition) { + if ($condition_plugin_id == 'domain' && (empty($condition['context_mapping']['domain']) || $condition['context_mapping']['domain'] !== $new_context_id)) { + $condition['context_mapping']['domain'] = $new_context_id; + $update_block = TRUE; + $update_list[] = $block_config_name; + } + } + } + if ($update_block) { + $block->set('visibility', $visibility); + $block->save(TRUE); + } + } + if (empty($update_list)) { + return t('No blocks updated.'); } + else { + return t('Updated @count blocks: @blocks', ['@count' => count($update_list), '@blocks' => implode(', ', $update_list)]); + } +} + +/** + * Ensure that the update to block visibility was applied properly. + */ +function domain_update_8003() { + domain_update_8002(); } diff --git a/domain/domain.links.task.yml b/domain/domain.links.task.yml index e6682109..dfce8c50 100644 --- a/domain/domain.links.task.yml +++ b/domain/domain.links.task.yml @@ -6,3 +6,13 @@ domain.settings: title: Domain settings route_name: domain.settings base_route: domain.admin +entity.domain.edit_form: + route_name: entity.domain.edit_form + base_route: entity.domain.edit_form + title: Edit + weight: 10 +entity.domain.delete_form: + route_name: entity.domain.delete_form + base_route: entity.domain.edit_form + title: Delete + weight: 20 diff --git a/domain/domain.module b/domain/domain.module index 6ba0b0d2..3d6ed5cd 100644 --- a/domain/domain.module +++ b/domain/domain.module @@ -5,30 +5,20 @@ * Defines a Domain concept for use with Drupal. */ +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; -use Drupal\domain\DomainInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Component\Utility\Html; -use Drupal\field\Entity\FieldStorageConfig; +use Drupal\domain\DomainInterface; /** - * The name of the node access control field. + * The name of the admin access control field. + * + * @deprecated This constant will be replaced in the final release by + * Drupal\domain\DomainInterface::DOMAIN_ADMIN_FIELD. */ const DOMAIN_ADMIN_FIELD = 'field_domain_admin'; -/** - * Implements hook_entity_bundle_info(). - */ -function domain_entity_bundle_info() { - $bundles['domain']['domain'] = array( - 'label' => t('Domain record'), - 'admin' => array( - 'real path' => 'admin/config/domain', - ), - ); - return $bundles; -} - /** * Entity URI callback. * @@ -84,11 +74,10 @@ function domain_token_info() { /** * Implements hook_tokens(). */ -function domain_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) { +function domain_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { return \Drupal::service('domain.token')->getTokens($type, $tokens, $data, $options, $bubbleable_metadata); } - /** * Implements hook_preprocess_HOOK() for html.html.twig. */ @@ -97,91 +86,15 @@ function domain_preprocess_html(array &$variables) { $config = \Drupal::config('domain.settings'); if ($string = $config->get('css_classes')) { $token = \Drupal::token(); - // Prepare the class properly. - $variables['attributes']['class'][] = Html::getClass($token->replace($string)); - } -} - -/** - * Creates our fields for an entity bundle. - * - * @param string $entity_type - * The entity type being created. Node and user are supported. - * @param string $bundle - * The bundle being created. - * @param string $handler - * The entity reference form widget to use. Defaults to 'default:domain'. - * See hook_domain_references_alter() for details. - * @param array $text - * The text to use for the field. Keys are: - * 'name' -- the lower-case, human-readable name of the entity. - * 'label' -- the form label for the all affiliates field. - * 'description' -- the help text for the all affiliates field. - * - * If calling this function for entities other than user or node, it is the - * caller's responsibility to provide this text. - * - * This function is here for convenience during installation. It is not really - * an API function. Modules wishing to add fields to non-node entities must - * provide their own field storage. See the field storage YML sample in - * tests/modules/domain_access_test for an example of field storage definitions. - * - * @TODO Update and abstract this function for use by other modules. - */ -function domain_confirm_fields($entity_type, $bundle, $handler = 'default:domain', $text = array()) { - // We have reports that importing config causes this function to fail. - try { - - $storage_id = $entity_type . '.' . DOMAIN_ADMIN_FIELD; - if (!$storage = \Drupal::entityTypeManager()->getStorage('field_storage_config')->load($storage_id)) { - $field_storage = [ - 'id' => $storage_id, - 'field_name' => DOMAIN_ADMIN_FIELD, - 'type' => 'entity_reference', - 'dependencies' => array('domain' , 'user'), - 'entity_type' => 'user', - 'cardinality' => -1, - 'module' => 'entity_reference', - 'settings' => ['target_type' => 'domain'], - ]; - $field_storage_config = FieldStorageConfig::create($field_storage); - - $field_storage_config->save(); - } - - $id = $storage_id = $entity_type . '.' . $bundle . '.' . DOMAIN_ADMIN_FIELD; - if (!$field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { - $field = array( - 'field_name' => DOMAIN_ADMIN_FIELD, - 'entity_type' => $entity_type, - 'label' => 'Domain administrator', - 'bundle' => $bundle, - // Users should not be required to be a domain editor. - 'required' => $entity_type !== 'user', - 'description' => 'Select the domains this user may administer', - 'default_value' => [], - 'settings' => [ - 'handler' => $handler, - 'handler_settings' => [ - 'sort' => ['field' => 'weight', 'direction' => 'ASC'], - ], - ], - ); - $field_config = \Drupal::entityTypeManager()->getStorage('field_config')->create($field); - $field_config->save(); + // Prepare the classes proparly, with one class per string. + $classes = explode(' ', trim($string)); + foreach ($classes as $class) { + // Ensure no leading or trailing space. + $class = trim($class); + if (!empty($class)) { + $variables['attributes']['class'][] = Html::getClass($token->replace($class)); + } } - // Tell the form system how to behave. Default to radio buttons. - // @TODO: This function is deprecated, but using the OO syntax is causing - // test fails. - entity_get_form_display($entity_type, $bundle, 'default') - ->setComponent(DOMAIN_ADMIN_FIELD, array( - 'type' => 'options_buttons', - 'weight' => 50, - )) - ->save(); - } - catch (Exception $e) { - \Drupal::logger('domain')->notice('Field installation failed.'); } } @@ -193,7 +106,7 @@ function domain_confirm_fields($entity_type, $bundle, $handler = 'default:domain function domain_form_user_form_alter(&$form, &$form_state, $form_id) { // Add the options hidden from the user silently to the form. $manager = \Drupal::service('domain.element_manager'); - $form = $manager->setFormOptions($form, $form_state, DOMAIN_ADMIN_FIELD); + $form = $manager->setFormOptions($form, $form_state, DomainInterface::DOMAIN_ADMIN_FIELD); } /** @@ -206,7 +119,7 @@ function domain_domain_references_alter($query, $account, $context) { // Do nothing. } elseif ($account->hasPermission('assign domain administrators')) { - $allowed = \Drupal::service('domain.element_manager')->getFieldValues($account, DOMAIN_ADMIN_FIELD); + $allowed = \Drupal::service('domain.element_manager')->getFieldValues($account, DomainInterface::DOMAIN_ADMIN_FIELD); $query->condition('id', array_keys($allowed), 'IN'); } else { @@ -215,3 +128,53 @@ function domain_domain_references_alter($query, $account, $context) { } } } + +/** + * Implements hook_views_data_alter(). + */ +function domain_views_data_alter(array &$data) { + $table = 'user__' . DomainInterface::DOMAIN_ADMIN_FIELD; + // Since domains are not stored in the database, relationships cannot be used. + unset($data[$table][DomainInterface::DOMAIN_ADMIN_FIELD]['relationship']); +} + +/** + * Implements hook_theme(). + */ +function domain_theme() { + return [ + 'domain_nav_block' => [ + 'render element' => 'items', + ], + ]; +} + +/** + * Prepares variables for block templates. + * + * Default template: domain-nav-block.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An array of labels and urls for use in the list. + * Properties used: 'label', 'url', 'active'. + */ +function template_preprocess_domain_nav_block(array &$variables) { + $variables['items'] = $variables['items']['#items']; +} + +/** + * Implements hook_hook_info(). + */ +function domain_hook_info() { + $hooks['domain_request_alter'] = [ + 'group' => 'domain', + ]; + $hooks['domain_validate_alter'] = [ + 'group' => 'domain', + ]; + $hooks['domain_references_alter'] = [ + 'group' => 'domain', + ]; + return $hooks; +} diff --git a/domain/domain.permissions.yml b/domain/domain.permissions.yml index 02581d37..c3b2d2f8 100644 --- a/domain/domain.permissions.yml +++ b/domain/domain.permissions.yml @@ -1,20 +1,32 @@ administer domains: title: 'Administer all domain records' + description: 'View, create, edit, and delete domain records. Allows all permissions for the module.' + restrict access: true access inactive domains: title: 'Access inactive domains' + description: 'Access domain URLs for domains marked as inactive.' + restrict access: true assign domain administrators: title: 'Assign additional administrators to assigned domains' + restrict access: true create domains: title: 'Create domain records' + restrict access: true edit assigned domains: title: 'Edit assigned domain records' + restrict access: true delete assigned domains: title: 'Delete assigned domain records' + restrict access: true +use domain nav block: + title: 'Access the domain navigation block' use domain switcher block: title: 'Access the domain switcher block' + restrict access: true view assigned domains: - title: 'View assigned domains' + title: 'View assigned domains in the administration list' view domain list: - title: 'View all registered domains' + title: 'View all registered domains in the administration list' view domain information: title: 'View debugging information for domain handling' + restrict access: true diff --git a/domain/domain.services.yml b/domain/domain.services.yml index 7ae17eeb..241fcdb3 100644 --- a/domain/domain.services.yml +++ b/domain/domain.services.yml @@ -3,7 +3,7 @@ services: class: Drupal\domain\Access\DomainAccessCheck tags: - { name: access_check } - arguments: ['@domain.negotiator', '@config.factory'] + arguments: ['@domain.negotiator', '@config.factory', '@path.matcher'] access_check.domain_route: class: Drupal\domain\Access\DomainRouteCheck tags: @@ -14,38 +14,25 @@ services: arguments: ['@domain.negotiator'] tags: - { name: 'context_provider' } - domain.creator: - class: Drupal\domain\DomainCreator - tags: - - { name: persist } - arguments: ['@domain.loader', '@domain.negotiator'] domain.element_manager: class: Drupal\domain\DomainElementManager - tags: - - { name: persist } - arguments: ['@domain.loader'] - domain.loader: - class: Drupal\domain\DomainLoader - tags: - - { name: persist } - arguments: ['@config.typed', '@config.factory'] + arguments: ['@entity_type.manager'] domain.negotiator: class: Drupal\domain\DomainNegotiator - tags: - - { name: persist } - arguments: ['@request_stack', '@module_handler', '@domain.loader', '@config.factory'] + arguments: ['@request_stack', '@module_handler', '@entity_type.manager', '@config.factory'] domain.subscriber: class: Drupal\domain\EventSubscriber\DomainSubscriber tags: - { name: event_subscriber } - arguments: ['@domain.negotiator', '@domain.loader', '@access_check.domain', '@current_user'] + arguments: ['@domain.negotiator', '@entity_type.manager', '@access_check.domain', '@current_user'] domain.token: class: Drupal\domain\DomainToken - tags: - - { name: persist } - arguments: ['@domain.loader', '@domain.negotiator'] + arguments: ['@entity_type.manager', '@domain.negotiator'] domain.validator: class: Drupal\domain\DomainValidator - tags: - - { name: persist } - arguments: ['@module_handler', '@config.factory', '@http_client'] + arguments: ['@module_handler', '@config.factory', '@http_client', '@entity_type.manager'] + domain.route_provider: + class: Drupal\domain\Routing\DomainRouteProvider + decorates: router.route_provider + decoration_priority: 10 + arguments: ['@domain.route_provider.inner', '@database', '@state', '@path.current', '@cache.data', '@path_processor_manager', '@cache_tags.invalidator', 'router', '@language_manager'] diff --git a/domain/drush.services.yml b/domain/drush.services.yml new file mode 100644 index 00000000..84d23f78 --- /dev/null +++ b/domain/drush.services.yml @@ -0,0 +1,5 @@ +services: + domain.commands: + class: \Drupal\domain\Commands\DomainCommands + tags: + - { name: drush.command } diff --git a/domain/migrations/d7_domain.yml b/domain/migrations/d7_domain.yml new file mode 100644 index 00000000..52e31b11 --- /dev/null +++ b/domain/migrations/d7_domain.yml @@ -0,0 +1,18 @@ +id: d7_domain +label: Domain Records +migration_tags: + - Drupal 7 +source: + plugin: d7_domain +process: + id: machine_name + name: sitename + hostname: subdomain + weight: weight + is_default: is_default + scheme: scheme + path: subdomain + status: valid +destination: + plugin: entity:domain + destination_module: domain diff --git a/domain/src/Access/DomainAccessCheck.php b/domain/src/Access/DomainAccessCheck.php index 7ad3014d..0a48aec4 100644 --- a/domain/src/Access/DomainAccessCheck.php +++ b/domain/src/Access/DomainAccessCheck.php @@ -5,6 +5,7 @@ use Drupal\Core\Access\AccessCheckInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Path\PathMatcherInterface; use Drupal\Core\Session\AccountInterface; use Drupal\domain\DomainNegotiatorInterface; use Symfony\Component\Routing\Route; @@ -28,17 +29,27 @@ class DomainAccessCheck implements AccessCheckInterface { */ protected $configFactory; + /** + * The path matcher service. + * + * @var \Drupal\Core\Path\PathMatcherInterface + */ + protected $pathMatcher; + /** * Constructs the object. * - * @param DomainNegotiatorInterface $negotiator + * @param \Drupal\domain\DomainNegotiatorInterface $negotiator * The domain negotiation service. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. + * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher + * The path matcher service. */ - public function __construct(DomainNegotiatorInterface $negotiator, ConfigFactoryInterface $config_factory) { + public function __construct(DomainNegotiatorInterface $negotiator, ConfigFactoryInterface $config_factory, PathMatcherInterface $path_matcher) { $this->domainNegotiator = $negotiator; $this->configFactory = $config_factory; + $this->pathMatcher = $path_matcher; } /** @@ -52,36 +63,29 @@ public function applies(Route $route) { * {@inheritdoc} */ public function checkPath($path) { - $allowed_paths = $this->configFactory->get('domain.settings')->get('login_paths', '/user/login\r\n/user/password'); - if (!empty($allowed_paths)) { - $paths = preg_split("(\r\n?|\n)", $allowed_paths); - } - if (!empty($paths) && in_array($path, $paths)) { - return FALSE; - } - return TRUE; + $allowed_paths = $this->configFactory->get('domain.settings')->get('login_paths'); + return !$this->pathMatcher->matchPath($path, $allowed_paths); } /** * {@inheritdoc} */ public function access(AccountInterface $account) { - /** @var \Drupal\domain\DomainInterface $domain */ $domain = $this->domainNegotiator->getActiveDomain(); // Is the domain allowed? // No domain, let it pass. if (empty($domain)) { - return AccessResult::allowed()->setCacheMaxAge(0); + return AccessResult::allowed()->addCacheTags(['url.site']); } // Active domain, let it pass. if ($domain->status()) { - return AccessResult::allowed()->setCacheMaxAge(0); + return AccessResult::allowed()->addCacheTags(['url.site']); } // Inactive domain, require permissions. else { - $permissions = array('administer domains', 'access inactive domains'); + $permissions = ['administer domains', 'access inactive domains']; $operator = 'OR'; - return AccessResult::allowedIfHasPermissions($account, $permissions, $operator)->setCacheMaxAge(0); + return AccessResult::allowedIfHasPermissions($account, $permissions, $operator)->addCacheTags(['url.site']); } } diff --git a/domain/src/Access/DomainListCheck.php b/domain/src/Access/DomainListCheck.php index a270f9c9..27108076 100644 --- a/domain/src/Access/DomainListCheck.php +++ b/domain/src/Access/DomainListCheck.php @@ -17,6 +17,7 @@ class DomainListCheck { * The account making the route request. * * @return \Drupal\Core\Access\AccessResult + * The access result. */ public static function viewDomainList(AccountInterface $account) { if ($account->hasPermission('administer domains') || $account->hasPermission('view domain list') || $account->hasPermission('view assigned domains')) { diff --git a/domain/src/Access/DomainRouteCheck.php b/domain/src/Access/DomainRouteCheck.php index 22a68178..a67066a3 100644 --- a/domain/src/Access/DomainRouteCheck.php +++ b/domain/src/Access/DomainRouteCheck.php @@ -12,8 +12,8 @@ * Determines access to routes based on domains. * * You can specify the '_domain' key on route requirements. If you specify a - * single domain, users with that domain with have access. If you specify multiple - * ones you can join them by using "+". + * single domain, users with that domain with have access. If you specify + * multiple ones you can join them by using "+". * * This access checker is separate from the global check used by inactive * domains. It is expressly for use with Views and other systems that need @@ -21,6 +21,13 @@ */ class DomainRouteCheck implements AccessInterface { + /** + * The key used by the routing requirement. + * + * @var string + */ + protected $requirementsKey = '_domain'; + /** * The Domain negotiator. * @@ -31,13 +38,20 @@ class DomainRouteCheck implements AccessInterface { /** * Constructs the object. * - * @param DomainNegotiatorInterface $negotiator + * @param \Drupal\domain\DomainNegotiatorInterface $negotiator * The domain negotiation service. */ public function __construct(DomainNegotiatorInterface $negotiator) { $this->domainNegotiator = $negotiator; } + /** + * {@inheritdoc} + */ + public function applies(Route $route) { + return $route->hasRequirement($this->requirementsKey); + } + /** * Checks access to a route with a _domain requirement. * @@ -53,7 +67,7 @@ public function __construct(DomainNegotiatorInterface $negotiator) { */ public function access(Route $route, AccountInterface $account) { // Requirements just allow strings, so this might be a comma separated list. - $string = $route->getRequirement('_domain'); + $string = $route->getRequirement($this->requirementsKey); $domain = $this->domainNegotiator->getActiveDomain(); // Since only one domain can be active per request, we only suport OR logic. $allowed = array_filter(array_map('trim', explode('+', $string))); diff --git a/domain/src/Commands/DomainCommandException.php b/domain/src/Commands/DomainCommandException.php new file mode 100644 index 00000000..0ab54726 --- /dev/null +++ b/domain/src/Commands/DomainCommandException.php @@ -0,0 +1,10 @@ +domainStorage()->loadMultipleSorted(); + + if (empty($domains)) { + $this->logger()->warning(dt('No domains have been created. Use "drush domain:add" to create one.')); + return new RowsOfFields([]); + } + + $keys = [ + 'weight', + 'name', + 'hostname', + 'response', + 'scheme', + 'status', + 'is_default', + 'domain_id', + 'id', + ]; + $rows = []; + /** @var \Drupal\domain\DomainInterface $domain */ + foreach ($domains as $domain) { + $row = []; + foreach ($keys as $key) { + switch ($key) { + case 'response': + try { + $v = $this->checkDomain($domain); + } + catch (TransferException $ex) { + $v = dt('500 - Failed'); + } + catch (Exception $ex) { + $v = dt('500 - Exception'); + } + if ($v >= 200 && $v <= 299) { + $v = dt('200 - OK'); + } + elseif ($v == 500) { + $v = dt('500 - No server'); + } + break; + + case 'status': + $v = $domain->get($key); + if (($options['inactive'] && $v) || ($options['active'] && !$v)) { + continue 3; + } + $v = !empty($v) ? dt('Active') : dt('Inactive'); + break; + + case 'is_default': + $v = $domain->get($key); + $v = !empty($v) ? dt('Default') : ''; + break; + + default: + $v = $domain->get($key); + break; + } + + $row[$key] = Html::escape($v); + } + $rows[] = $row; + } + return new RowsOfFields($rows); + } + + /** + * List general information about the domains on the site. + * + * @usage drush domain:info + * + * @command domain:info + * @aliases domain-info,dinf + * + * @return \Consolidation\OutputFormatters\StructuredData\PropertyList + * A structured list of domain information. + * + * @field-labels + * count: All Domains + * count_active: Active Domains + * default_id: Default Domain ID + * default_host: Default Domain hostname + * scheme: Fields in Domain entity + * domain_admin_entities: Domain admin entities + * @list-orientation true + * @format table + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function infoDomains() { + $default_domain = $this->domainStorage()->loadDefaultDomain(); + + // Load all domains: + $all_domains = $this->domainStorage()->loadMultiple(NULL); + $active_domains = []; + foreach ($all_domains as $domain) { + if ($domain->status()) { + $active_domains[] = $domain; + } + } + + $keys = [ + 'count', + 'count_active', + 'default_id', + 'default_host', + 'scheme', + ]; + $rows = []; + foreach ($keys as $key) { + $v = ''; + switch ($key) { + case 'count': + $v = count($all_domains); + break; + + case 'count_active': + $v = count($active_domains); + break; + + case 'default_id': + $v = '-unset-'; + if ($default_domain) { + $v = $default_domain->id(); + } + break; + + case 'default_host': + $v = '-unset-'; + if ($default_domain) { + $v = $default_domain->getHostname(); + } + break; + + case 'scheme': + $v = implode(', ', array_keys($this->domainStorage()->loadSchema())); + break; + } + + $rows[$key] = $v; + } + + // Display which entities are enabled for domain by checking for the fields. + $rows['domain_admin_entities'] = $this->getFieldEntities(DomainInterface::DOMAIN_ADMIN_FIELD); + + return new PropertyList($rows); + } + + /** + * Finds entities that reference a specific field. + * + * @param string $field_name + * The field name to lookup. + * + * @return string + * A comma-separated list of entities containing a specific field. + */ + public function getFieldEntities($field_name) { + $this->ensureEntityFieldMap(); + $domain_entities = []; + foreach ($this->entityFieldMap as $type => $fields) { + if (array_key_exists($field_name, $fields)) { + $domain_entities[] = $type; + } + } + return implode(', ', $domain_entities); + } + + /** + * Add a new domain to the site. + * + * @param string $hostname + * The domain hostname to register (e.g. example.com). + * @param string $name + * The name of the site (e.g. Domain Two). + * @param array $options + * An associative array of optional values. + * + * @option inactive + * Set the domain to inactive status if set. + * @option scheme + * Use indicated protocol for this domain, defaults to 'https'. Options: + * - http: normal http (no SSL). + * - https: secure https (with SSL). + * - variable: match the scheme used by the request. + * @option weight + * Set the order (weight) of the domain. + * @option is_default + * Set this domain as the default domain. + * @option validate + * Force a check of the URL response before allowing registration. + * + * @usage drush domain-add example.com 'My Test Site' + * @usage drush domain-add example.com 'My Test Site' --scheme=https --inactive + * @usage drush domain-add example.com 'My Test Site' --weight=10 + * @usage drush domain-add example.com 'My Test Site' --validate + * + * @command domain:add + * @aliases domain-add + * + * @return string + * The entity id of the created domain. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function add($hostname, $name, array $options = ['weight' => NULL, 'scheme' => NULL]) { + // Validate the weight arg. + if (!empty($options['weight']) && !is_numeric($options['weight'])) { + throw new DomainCommandException( + dt('Domain weight "!weight" must be a number', + ['!weight' => !empty($options['weight']) ? $options['weight'] : '']) + ); + } + + // Validate the scheme arg. + if (!empty($options['scheme']) && + ($options['scheme'] !== 'http' && $options['scheme'] !== 'https' && $options['scheme'] !== 'variable') + ) { + throw new DomainCommandException( + dt('Scheme name "!scheme" not known', + ['!scheme' => !empty($options['scheme']) ? $options['scheme'] : '']) + ); + } + + $domains = $this->domainStorage()->loadMultipleSorted(); + $start_weight = count($domains) + 1; + $values = [ + 'hostname' => $hostname, + 'name' => $name, + 'status' => empty($options['inactive']), + 'scheme' => empty($options['scheme']) ? 'http' : $options['scheme'], + 'weight' => empty($options['weight']) ? $start_weight : $options['weight'], + 'is_default' => !empty($options['is_default']), + 'id' => $this->domainStorage()->createMachineName($hostname), + ]; + /** @var \Drupal\domain\DomainInterface */ + $domain = $this->domainStorage()->create($values); + + // Check for hostname validity. This is required. + $valid = $this->validateDomain($domain); + if (!empty($valid)) { + throw new DomainCommandException( + dt('Hostname is not valid. !errors', + ['!errors' => implode(" ", $valid)]) + ); + } + // Check for hostname and id uniqueness. + foreach ($domains as $existing) { + if ($hostname == $existing->getHostname()) { + throw new DomainCommandException( + dt('No domain created. Hostname is a duplicate of !hostname.', + ['!hostname' => $hostname]) + ); + } + if ($values['id'] == $existing->id()) { + throw new DomainCommandException( + dt('No domain created. Id is a duplicate of !id.', + ['!id' => $existing->id()]) + ); + } + } + + $validate_response = (bool) $options['validate']; + if ($this->createDomain($domain, $validate_response)) { + return dt('Created the !hostname with machine id !id.', ['!hostname' => $values['hostname'], '!id' => $values['id']]); + } + else { + return dt('No domain created.'); + } + } + + /** + * Delete a domain from the site. + * + * Deletes the domain from the Drupal configuration and optionally reassign + * content and/or profiles associated with the deleted domain to another. + * The domain marked as default cannot be deleted: to achieve this goal, + * mark another, possibly newly created, domain as the default domain, then + * delete the old default. + * + * The usage example descriptions are based on starting with three domains: + * - id:19476, machine: example_com, domain: example.com + * - id:29389, machine: example_org, domain: example.org (default) + * - id:91736, machine: example_net, domain: example.net + * + * @param string $domain_id + * The numeric id, machine name, or hostname of the domain to delete. The + * value "all" is taken to mean delete all except the default domain. + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * + * @usage drush domain:delete example.com + * Delete the domain example.com, assigning its content and users to + * the default domain, example.org. + * + * @usage drush domain:delete --content-assign=ignore example.com + * Delete the domain example.com, leaving its content untouched but + * assigning its users to the default domain. + * + * @usage drush domain:delete --content-assign=example_net --users-assign=example_net + * example.com Delete the domain example.com, assigning its content and + * users to the example.net domain. + * + * @usage drush domain:delete --dryrun 19476 + * Show the effects of delete the domain example.com and assigning its + * content and users to the default domain, example.org, but not doing so. + * + * @usage drush domain:delete --chatty example_net + * Verbosely Delete the domain example.net and assign its content and users + * to the default domain, example.org. + * + * @usage drush domain-delete --chatty all + * Verbosely Delete the domains example.com and example.net and assign + * their content and users to the default domain, example.org. + * + * @option chatty + * Document each step as it is performed. + * @option dryrun + * Do not do anything, but explain what would be done. Implies --chatty. + * @option users-assign + * Values "prompt", "ignore", "default", , Reassign user accounts + * associated with the the domain being deleted to the default domain, + * to the domain whose machine name is , or leave the user accounts + * alone (and so inaccessible in the normal way). The default value is + * 'prompt': ask which domain to use. + * + * @command domain:delete + * @aliases domain-delete + * + * @throws \Drupal\domain\Commands\DomainCommandException + * + * @see https://github.com/consolidation/annotated-command#option-event-hook + */ + public function delete($domain_id, array $options = ['users-assign' => NULL, 'dryrun' => NULL, 'chatty' => NULL]) { + if (is_null($options['users-assign'])) { + $policy_users = 'prompt'; + } + + $this->isDryRun = (bool) $options['dryrun']; + + // Get current domain list and perform validation checks. + $default_domain = $this->domainStorage()->loadDefaultDomain(); + $all_domains = $this->domainStorage()->loadMultipleSorted(NULL); + + if (empty($all_domains)) { + throw new DomainCommandException('There are no configured domains.'); + } + if (empty($domain_id)) { + throw new DomainCommandException('You must specify a domain to delete.'); + } + + // Determine which domains to be deleted. + if ($domain_id === 'all') { + $domains = $all_domains; + if (empty($domains)) { + $this->logger()->info(dt('There are no domains to delete.')); + return; + } + $really = $this->io()->confirm(dt('This action cannot be undone. Continue?:'), FALSE); + if (empty($really)) { + return; + } + // TODO: handle deletion of all domains. + $policy_users = "ignore"; + $message = dt('All domain records have been deleted.'); + } + elseif ($domain = $this->getDomainFromArgument($domain_id)) { + if ($domain->isDefault()) { + throw new DomainCommandException('The primary domain may not be deleted. + Use drush domain:default to set a new default domain.'); + } + $domains = [$domain]; + $message = dt('Domain record !domain deleted.', + ['!domain' => $domain->id()] + ); + } + + if (!empty($options['users-assign'])) { + if (in_array($options['users-assign'], $this->reassignmentPolicies, TRUE)) { + $policy_users = $options['users-assign']; + } + } + + $delete_options = [ + 'entity_filter' => 'user', + 'policy' => $policy_users, + 'field' => DomainInterface::DOMAIN_ADMIN_FIELD, + ]; + + if ($policy_users !== 'ignore') { + $messages[] = $this->doReassign($domain, $delete_options); + } + + // Fire any registered hooks for deletion, passing them current imput. + $handlers = $this->getCustomEventHandlers('domain-delete'); + $messages = []; + foreach ($handlers as $handler) { + $messages[] = $handler($domain, $options); + } + + $this->deleteDomain($domains, $options); + + if ($messages) { + $message .= "\n" . implode("\n", $messages); + } + $this->logger()->info($message); + return $message; + } + + /** + * Handles reassignment of entities to another domain. + * + * This method includes necessary UI elements if the user is prompted to + * choose a new domain. + * + * @param Drupal\domain\DomainInterface $target_domain + * The domain selected for deletion. + * @param array $delete_options + * A selection of options for deletion, defined in reassignLinkedEntities(). + */ + public function doReassign(DomainInterface $target_domain, array $delete_options) { + $policy = $delete_options['policy']; + $default_domain = $this->domainStorage()->loadDefaultDomain(); + $all_domains = $this->domainStorage()->loadMultipleSorted(NULL); + + // Perform the 'prompt' for a destination domain. + if ($policy === 'prompt') { + // Make a list of the eligible destination domains in form id -> name. + $noassign_domain = [$target_domain->id()]; + + $reassign_list = $this->filterDomains($all_domains, $noassign_domain); + $reassign_base = [ + 'ignore' => dt('Do not reassign'), + 'default' => dt('Reassign to default domain'), + ]; + $reassign_list = array_map( + function (DomainInterface $d) { + return $d->getHostname(); + }, + $reassign_list + ); + $reassign_list = array_merge($reassign_base, $reassign_list); + $policy = $this->io()->choice(dt('Reassign @type field @field data to:', ['@type' => $delete_options['entity_filter'], '@field' => $delete_options['field']]), $reassign_list); + } + elseif ($policy === 'default') { + $policy = $default_domain->id(); + } + if ($policy !== 'ignore') { + $delete_options['policy'] = $policy; + $target = [$target_domain]; + $count = $this->reassignLinkedEntities($target, $delete_options); + return dt('@count @type entities updated field @field.', + [ + '@count' => $count, + '@type' => $delete_options['entity_filter'], + '@field' => $delete_options['field'], + ] + ); + } + } + + /** + * Tests domains for proper response. + * + * If run from a subfolder, you must specify the --uri. + * + * @param string $domain_id + * The machine name or hostname of the domain to make default. + * + * @usage drush domain-test + * @usage drush domain-test example.com + * + * @command domain:test + * @aliases domain-test + * + * @field-labels + * id: Machine name + * url: URL + * response: HTTP Response + * @default-fields id,url,response + * + * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields + * Tabled output. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function test($domain_id = NULL) { + if (is_null($domain_id)) { + $domains = $this->domainStorage()->loadMultipleSorted(); + } + else { + if ($domain = $this->getDomainFromArgument($domain_id)) { + $domains = [$domain]; + } + else { + throw new DomainCommandException(dt('Domain @domain not found.', + ['@domain' => $options['domain']])); + } + } + + $rows = []; + foreach ($domains as $domain) { + $rows[] = [ + 'id' => $domain->id(), + 'url' => $domain->getPath(), + 'response' => $domain->getResponse(), + ]; + } + + return new RowsOfFields($rows); + } + + /** + * Sets the default domain. + * + * @param string $domain_id + * The machine name or hostname of the domain to make default. + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * @option validate + * Force a check of the URL response before allowing registration. + * @usage drush domain-default www.example.com + * @usage drush domain-default example_org + * @usage drush domain-default www.example.org --validate=1 + * + * @command domain:default + * @aliases domain-default + * + * @return string + * The machine name of the default domain. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function defaultDomain($domain_id, array $options = ['validate' => NULL]) { + // Resolve the domain. + if (!empty($domain_id) && $domain = $this->getDomainFromArgument($domain_id)) { + $validate = ($options['validate']) ? 1 : 0; + $domain->addProperty('validate_url', $validate); + if ($error = $this->checkHttpResponse($domain)) { + throw new DomainCommandException(dt('Unable to verify domain !domain: !error', + ['!domain' => $domain->getHostname(), '!error' => $error])); + } + else { + $domain->saveDefault(); + } + } + + // Now, ask for the current default, so we know if it worked. + $domain = $this->domainStorage()->loadDefaultDomain(); + if ($domain->status()) { + $this->logger()->info(dt('!domain set to primary domain.', + ['!domain' => $domain->getHostname()])); + } + else { + $this->logger()->warning(dt('!domain set to primary domain, but is also inactive.', + ['!domain' => $domain->getHostname()])); + } + return $domain->id(); + } + + /** + * Deactivates the domain. + * + * @param string $domain_id + * The numeric id or hostname of the domain to disable. + * @usage drush domain-disable example.com + * @usage drush domain-disable 1 + * + * @command domain:disable + * @aliases domain-disable + * + * @return string + * Message to print. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function disable($domain_id) { + // Resolve the domain. + if ($domain = $this->getDomainFromArgument($domain_id)) { + if ($domain->status()) { + $domain->disable(); + $this->logger()->info(dt('!domain has been disabled.', + ['!domain' => $domain->getHostname()])); + return dt('Disabled !domain.', ['!domain' => $domain->getHostname()]); + } + else { + $this->logger()->info(dt('!domain is already disabled.', + ['!domain' => $domain->getHostname()])); + return dt('!domain is already disabled.', + ['!domain' => $domain->getHostname()] + ); + } + } + return dt('No matching domain record found.'); + } + + /** + * Activates the domain. + * + * @param string $domain_id + * The numeric id or hostname of the domain to enable. + * @usage drush domain-disable example.com + * @usage drush domain-enable 1 + * + * @command domain:enable + * @aliases domain-enable + * + * @return string + * The message to print. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function enable($domain_id) { + // Resolve the domain. + if ($domain = $this->getDomainFromArgument($domain_id)) { + if (!$domain->status()) { + $domain->enable(); + $this->logger()->info(dt('!domain has been enabled.', + ['!domain' => $domain->getHostname()])); + return dt('Enabled !domain.', ['!domain' => $domain->getHostname()]); + } + else { + $this->logger()->info(dt('!domain is already enabled.', + ['!domain' => $domain->getHostname()])); + return dt('!domain is already enabled.', + ['!domain' => $domain->getHostname()] + ); + } + } + return dt('No matching domain record found.'); + } + + /** + * Changes a domain label. + * + * @param string $domain_id + * The machine name or hostname of the domain to relabel. + * @param string $name + * The name to use for the domain. + * @usage drush domain-name example.com Foo + * @usage drush domain-name 1 Foo + * + * @command domain:name + * @aliases domain-name + * + * @return string + * The message to print. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function renameDomain($domain_id, $name) { + // Resolve the domain. + if ($domain = $this->getDomainFromArgument($domain_id)) { + $domain->saveProperty('name', $name); + return dt('Renamed !domain to !name.', ['!domain' => $domain->getHostname(), '!name' => $domain->label()]); + } + return dt('No matching domain record found.'); + } + + /** + * Changes a domain scheme. + * + * @param string $domain_id + * The machine name or hostname of the domain to change. + * @param string $scheme + * The scheme to use for the domain: http, https, or variable. + * + * @usage drush domain-scheme example.com http + * @usage drush domain-scheme example_com https + * + * @command domain:scheme + * @aliases domain-scheme + * + * @return string + * The message to print. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function scheme($domain_id, $scheme = NULL) { + $new_scheme = NULL; + + // Resolve the domain. + if ($domain = $this->getDomainFromArgument($domain_id)) { + if (!empty($scheme)) { + // Set with a value. + $new_scheme = $scheme; + } + else { + // Prompt for selection. + $new_scheme = $this->io()->choice(dt('Select the default http scheme:'), + [ + 'http' => 'http', + 'https' => 'https', + 'variable' => 'variable', + ]); + } + + // If we were asked to change scheme, validate the value and do so. + if (!empty($new_scheme)) { + switch ($new_scheme) { + case 'http': + $new_scheme = 'http'; + break; + + case 'https': + $new_scheme = 'https'; + break; + + case 'variable': + $new_scheme = 'variable'; + break; + + default: + throw new DomainCommandException( + dt('Scheme name "!scheme" not known.', ['!scheme' => $new_scheme]) + ); + } + $domain->saveProperty('scheme', $new_scheme); + } + + // Return the (new | current) scheme for this domain. + return dt('Scheme is now to "!scheme." for !domain', + ['!scheme' => $domain->get('scheme'), '!domain' => $domain->id()] + ); + } + + // We couldn't find the domain, so fail. + throw new DomainCommandException( + dt('Domain name "!domain" not known.', ['!domain' => $domain_id]) + ); + } + + /** + * Generate domains for testing. + * + * @param string $primary + * The primary domain to use. This will be created and used for + * *.example.com hostnames. + * @param array $options + * An associative array of options whose values come from cli, aliases, + * config, etc. + * + * @option count + * The count of extra domains to generate. Default is 15. + * @option empty + * Pass empty=1 to truncate the {domain} table before creating records. + * @option scheme + * Options are http | https | variable + * @usage drush domain-generate example.com + * @usage drush domain-generate example.com --count=25 + * @usage drush domain-generate example.com --count=25 --empty=1 + * @usage drush domain-generate example.com --count=25 --empty=1 --scheme=https + * @usage drush gend + * @usage drush gend --count=25 + * @usage drush gend --count=25 --empty=1 + * @usage drush gend --count=25 --empty=1 --scheme=https + * + * @command domain:generate + * @aliases gend,domgen,domain-generate + * + * @return string + * The message to print. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + public function generate($primary = 'example.com', array $options = ['count' => NULL, 'empty' => NULL, 'scheme' => 'http']) { + // Check the number of domains to create. + $count = $options['count']; + if (is_null($count)) { + $count = 15; + } + + $domains = $this->domainStorage()->loadMultiple(NULL); + if (!empty($options['empty'])) { + $this->domainStorage()->delete($domains); + $domains = $this->domainStorage()->loadMultiple(NULL); + } + // Ensure we don't duplicate any domains. + $existing = []; + if (!empty($domains)) { + foreach ($domains as $domain) { + $existing[] = $domain->getHostname(); + } + } + // Set up one.* and so on. + $names = [ + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + 'foo', + 'bar', + 'baz', + ]; + // Set the creation array. + $new = [$primary]; + foreach ($names as $name) { + $new[] = $name . '.' . $primary; + } + // Include a non hostname. + $new[] = 'my' . $primary; + // Filter against existing so we can count correctly. + $prepared = []; + foreach ($new as $key => $value) { + if (!in_array($value, $existing, TRUE)) { + $prepared[] = $value; + } + } + + // Add any test domains that have numeric prefixes. We don't expect these + // URLs to work, and mainly use them for testing the user interface. + $start = 1; + foreach ($existing as $exists) { + $name = explode('.', $exists); + if (substr_count($name[0], 'test') > 0) { + $num = (int) str_replace('test', '', $name[0]) + 1; + if ($num > $start) { + $start = $num; + } + } + } + $needed = $count - count($prepared) + $start; + for ($i = $start; $i <= $needed; $i++) { + $prepared[] = 'test' . $i . '.' . $primary; + } + // Get the initial item weight for sorting. + $start_weight = count($domains); + $prepared = array_slice($prepared, 0, $count); + $list = []; + + // Create the domains. + foreach ($prepared as $key => $item) { + $hostname = mb_strtolower($item); + $values = [ + 'name' => ($item != $primary) ? ucwords(str_replace(".$primary", '', $item)) : \Drupal::config('system.site')->get('name'), + 'hostname' => $hostname, + 'scheme' => $options['scheme'], + 'status' => 1, + 'weight' => ($item != $primary) ? $key + $start_weight + 1 : -1, + 'is_default' => 0, + 'id' => $this->domainStorage()->createMachineName($hostname), + ]; + $domain = $this->domainStorage()->create($values); + $domain->save(); + $list[] = dt('Created @domain.', ['@domain' => $domain->getHostname()]); + } + + // If nothing created, say so. + if (empty($prepared)) { + return dt('No new domains were created.'); + } + else { + return dt("Created @count new domains:\n@list", + ['@count' => count($prepared), '@list' => implode("\n", $list)]); + } + } + + /** + * Gets a domain storage object or throw an exception. + * + * Note that domain can run very early in the bootstrap, so we cannot + * reliably inject this service. + * + * @return \Drupal\domain\DomainStorageInterface + * The domain storage handler. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + protected function domainStorage() { + if (!is_null($this->domainStorage)) { + return $this->domainStorage; + } + + try { + $this->domainStorage = \Drupal::entityTypeManager()->getStorage('domain'); + } + catch (PluginNotFoundException $e) { + throw new DomainCommandException('Unable to get domain: no storage', $e); + } + catch (InvalidPluginDefinitionException $e) { + throw new DomainCommandException('Unable to get domain: bad storage', $e); + } + + return $this->domainStorage; + } + + /** + * Loads a domain based on a string identifier. + * + * @param string $argument + * The machine name or the hostname of an existing domain. + * + * @return \Drupal\domain\DomainInterface + * The domain entity. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + protected function getDomainFromArgument($argument) { + + // Try loading domain assuming arg is a machine name. + $domain = $this->domainStorage()->load($argument); + if (!$domain) { + // Try loading assuming it is a host name. + $domain = $this->domainStorage()->loadByHostname($argument); + } + + // domain_id (an INT) is only used internally because the Node Access + // system demands the use of numeric keys. It should never be used to load + // or identify domain records. Use the machine_name or hostname instead. + if (!$domain) { + throw new DomainCommandException( + dt('Domain record could not be found from "!a".', ['!a' => $argument]) + ); + } + + return $domain; + } + + /** + * Filters a list of domains by specific exclude list. + * + * @param \Drupal\domain\DomainInterface[] $domains + * List of domains. + * @param string[] $exclude + * List of domain id to exclude from the list. + * @param \Drupal\domain\DomainInterface[] $initial + * Initial value of list that will be returned. + * + * @return array + * An array of domains. + */ + protected function filterDomains(array $domains, array $exclude, array $initial = []) { + foreach ($domains as $domain) { + // Exclude unwanted domains. + if (!in_array($domain->id(), $exclude, FALSE)) { + $initial[$domain->id()] = $domain; + } + } + return $initial; + } + + /** + * Checks the domain response. + * + * @param \Drupal\domain\DomainInterface $domain + * The domain to check. + * @param bool $validate_url + * True to validate this domain by performing a URL lookup; False to skip + * the checks. + * + * @return bool + * True if the domain resolves properly, or we are not checking, + * False otherwise. + */ + protected function checkHttpResponse(DomainInterface $domain, $validate_url = FALSE) { + // Ensure the url is rebuilt. + if ($validate_url) { + $code = $this->checkDomain($domain); + // Some sort of success: + return ($code >= 200 && $code <= 299); + } + // Not validating, return FALSE. + return FALSE; + } + + /** + * Helper function: check a domain is responsive and create it. + * + * @param \Drupal\domain\DomainInterface $domain + * The (as yet unsaved) domain to create. + * @param bool $check_response + * Indicates that registration should not be allowed unless the server + * returns a 200 response. + * + * @return bool + * TRUE or FALSE indicating success of the action. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + protected function createDomain(DomainInterface $domain, $check_response = FALSE) { + if ($check_response) { + $valid = $this->checkHttpResponse($domain, TRUE); + if (!$valid) { + throw new DomainCommandException( + dt('The server did not return a 200 response for !d. Domain creation failed. Remove the --validate flag to save this domain.', ['!d' => $domain->getHostname()]) + ); + } + } + else { + try { + $domain->save(); + } + catch (EntityStorageException $e) { + throw new DomainCommandException('Unable to save domain', $e); + } + + if ($domain->getDomainId()) { + $this->logger()->info(dt('Created @name at @domain.', + ['@name' => $domain->label(), '@domain' => $domain->getHostname()])); + return TRUE; + } + else { + $this->logger()->error(dt('The request could not be completed.')); + } + } + return FALSE; + } + + /** + * Checks if a domain exists by trying to do an http request to it. + * + * @param \Drupal\domain\DomainInterface $domain + * The domain to validate for syntax and uniqueness. + * + * @return int + * The server response code for the request. + * + * @see domain_validate() + */ + protected function checkDomain(DomainInterface $domain) { + /** @var \Drupal\domain\DomainValidatorInterface $validator */ + $validator = \Drupal::service('domain.validator'); + return $validator->checkResponse($domain); + } + + /** + * Validates a domain meets the standards for a hostname. + * + * @param \Drupal\domain\DomainInterface $domain + * The domain to validate for syntax and uniqueness. + * + * @return string[] + * Array of strings indicating issues found. + * + * @see domain_validate() + */ + protected function validateDomain(DomainInterface $domain) { + /** @var \Drupal\domain\DomainValidatorInterface $validator */ + $validator = \Drupal::service('domain.validator'); + return $validator->validate($domain->getHostname()); + } + + /** + * Deletes a domain record. + * + * @param \Drupal\domain\DomainInterface[] $domains + * The domains to delete. + * + * @throws \Drupal\domain\Commands\DomainCommandException + * @throws \UnexpectedValueException + */ + protected function deleteDomain(array $domains) { + foreach ($domains as $domain) { + if (!$domain instanceof DomainInterface) { + throw new StorageException('deleting domains: value is not a domain'); + } + $hostname = $domain->getHostname(); + + if ($this->isDryRun) { + $this->logger()->info(dt('DRYRUN: Domain record @domain deleted.', + ['@domain' => $hostname])); + continue; + } + + try { + $domain->delete(); + } + catch (EntityStorageException $e) { + throw new DomainCommandException(dt('Unable to delete domain: @domain', + ['@domain' => $hostname]), $e); + } + $this->logger()->info(dt('Domain record @domain deleted.', + ['@domain' => $hostname])); + } + } + + /** + * Returns a list of the entity types that are domain enabled. + * + * A domain-enabled entity is defined here as an entity type that includes + * the domain access field(s). + * + * @param string $using_field + * The specific field name to look for. + * + * @return string[] + * List of entity machine names that support domain references. + */ + protected function findDomainEnabledEntities($using_field = DomainInterface::DOMAIN_ADMIN_FIELD) { + $this->ensureEntityFieldMap(); + $entities = []; + foreach ($this->entityFieldMap as $type => $fields) { + if (array_key_exists($using_field, $fields)) { + $entities[] = $type; + } + } + return $entities; + } + + /** + * Determines whether or not a given entity is domain-enabled. + * + * @param string $entity_type + * The machine name of the entity. + * @param string $field + * The name of the field to check for existence. + * + * @return bool + * True if this type of entity has a domain field. + */ + protected function entityHasDomainField($entity_type, $field = DomainInterface::DOMAIN_ADMIN_FIELD) { + // Try to avoid repeated calls to getFieldMap(), assuming it's expensive. + $this->ensureEntityFieldMap(); + return array_key_exists($field, $this->entityFieldMap[$entity_type]); + } + + /** + * Ensure the local entity field map has been defined. + * + * Asking for the entity field map cause a lot of lookup, so we lazily + * fetch it and then remember it to avoid repeated checks. + */ + protected function ensureEntityFieldMap() { + // Try to avoid repeated calls to getFieldMap() assuming it's expensive. + if (empty($this->entityFieldMap)) { + $entity_field_manager = \Drupal::service('entity_field.manager'); + $this->entityFieldMap = $entity_field_manager->getFieldMap(); + } + } + + /** + * Enumerate entity instances of the supplied type and domain. + * + * @param string $entity_type + * The entity type name, e.g. 'node'. + * @param string $domain_id + * The machine name of the domain to enumerate. + * @param string $field + * The field to manipulate in the entity, e.g. + * DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD. + * @param bool $just_count + * Flag to return a count rather than a list. + * + * @return int|string[] + * List of entity IDs for the selected domain or a count of domains. + */ + protected function enumerateDomainEntities($entity_type, $domain_id, $field, $just_count = FALSE) { + if (!$this->entityHasDomainField($entity_type, $field)) { + $this->logger()->info('Entity type @entity_type does not have field @field, so none found.', + [ + '@entity_type' => $entity_type, + '@field' => $field, + ] + ); + return []; + } + + $efq = \Drupal::entityQuery($entity_type); + // Don't access check or we wont get all of the possible entities moved. + $efq->accessCheck(FALSE); + $efq->condition($field, $domain_id, '='); + if ($just_count) { + $efq->count(); + } + return $efq->execute(); + } + + /** + * Reassign old_domain entities, of the supplied type, to the new_domain. + * + * @param string $entity_type + * The entity type name, e.g. 'node'. + * @param string $field + * The field to manipulate in the entity, e.g. + * DomainInterface::DOMAIN_ADMIN_FIELD. + * @param \Drupal\domain\DomainInterface $old_domain + * The domain the entities currently belong to. It is not an error for + * entity ids to be passed in that are not in this domain, though of course + * not very useful. + * @param \Drupal\domain\DomainInterface $new_domain + * The domain the entities should now belong to: When an entity belongs to + * the old_domain, this domain replaces it. + * @param array $ids + * List of entity IDs for the selected domain and all of type $entity_type. + * + * @return int + * A count of the number of entities changed. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function reassignEntities($entity_type, $field, DomainInterface $old_domain, DomainInterface $new_domain, array $ids) { + $entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type); + $entities = $entity_storage->loadMultiple($ids); + + foreach ($entities as $entity) { + $changed = FALSE; + if (!$entity->hasField($field)) { + continue; + } + // Multivalue fields are used, so check each one. + foreach ($entity->get($field) as $k => $item) { + if ($item->target_id == $old_domain->id()) { + + if ($this->isDryRun) { + $this->logger()->info(dt('DRYRUN: Update domain membership for entity @id to @new.', + [ + '@id' => $entity->id(), + '@new' => $new_domain->id(), + ] + ) + ); + // Don't set changed, so don't save either. + continue; + } + + $changed = TRUE; + $item->target_id = $new_domain->id(); + } + } + if ($changed) { + $entity->save(); + } + } + return count($entities); + } + + /** + * Return the Domain object corresponding to a policy string. + * + * @param string $policy + * In general one of 'prompt' | 'default' | 'ignore' or a domain entity + * machine name, but this function does not process 'prompt'. + * + * @return \Drupal\Core\Entity\EntityInterface|\Drupal\domain\DomainInterface|null + * The requested domain or NULL if not found. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + protected function getDomainInstanceFromPolicy($policy) { + switch ($policy) { + // Use the Default Domain machine name. + case 'default': + $new_domain = $this->domainStorage()->loadDefaultDomain(); + break; + + // Ask interactively for a Domain machine name. + case 'prompt': + case 'ignore': + return NULL; + + // Use this (specified) Domain machine name. + default: + $new_domain = $this->domainStorage()->load($policy); + break; + } + + return $new_domain; + } + + /** + * Reassign entities of the supplied type to the $policy domain. + * + * @param array $options + * Drush options sent to the command. An array such as the following: + * [ + * 'entity_filter' => 'node', + * 'policy' => 'prompt' | 'default' | 'ignore' | {domain_id} + * 'field' => DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, + * ]; + * The caller is expected to provide this information. + * @param array $domains + * Array of domain objects to reassign content away from. + * + * @return int + * The count of updated entities. + * + * @throws \Drupal\domain\Commands\DomainCommandException + */ + protected function reassignLinkedEntities(array $domains, array $options) { + $count = 0; + $field = $options['field']; + $entity_typenames = $this->findDomainEnabledEntities($field); + + $new_domain = $this->getDomainInstanceFromPolicy($options['policy']); + if (empty($new_domain)) { + throw new DomainCommandException('invalid destination domain'); + } + + // Loop through each entity type. + $exceptions = FALSE; + foreach ($entity_typenames as $name) { + if (empty($options['entity_filter']) || $options['entity_filter'] === $name) { + + // For each domain being reassigned from... + foreach ($domains as $domain) { + $ids = $this->enumerateDomainEntities($name, $domain->id(), $field); + if (!empty($ids)) { + try { + if ($options['chatty']) { + $this->logger()->info('Reassigning @count @entity_name entities to @domain', + [ + '@entity_name' => '', + '@count' => count($ids), + '@domain' => $new_domain->id(), + ] + ); + } + $count = $this->reassignEntities($name, $field, $domain, $new_domain, $ids); + } + catch (PluginException $e) { + $exceptions = TRUE; + $this->logger()->error('Unable to reassign content to @new_domain: plugin exception: @ex', + [ + '@ex' => $e->getMessage(), + '@new_domain' => $new_domain->id(), + ] + ); + } + catch (EntityStorageException $e) { + $exceptions = TRUE; + $this->logger()->error('Unable to reassign content to @new_domain: storage exception: @ex', + [ + '@ex' => $e->getMessage(), + '@new_domain' => $new_domain->id(), + ] + ); + } + } + } + } + } + if ($exceptions) { + throw new DomainCommandException('Errors encountered during reassign.'); + } + + return $count; + } + +} diff --git a/domain/src/ContextProvider/CurrentDomainContext.php b/domain/src/ContextProvider/CurrentDomainContext.php index 8c61ad58..09081e12 100644 --- a/domain/src/ContextProvider/CurrentDomainContext.php +++ b/domain/src/ContextProvider/CurrentDomainContext.php @@ -2,12 +2,13 @@ namespace Drupal\domain\ContextProvider; -use Drupal\domain\DomainNegotiatorInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\ContextProviderInterface; +use Drupal\Core\Plugin\Context\EntityContext; +use Drupal\Core\Plugin\Context\EntityContextDefinition; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\domain\DomainNegotiatorInterface; /** * Provides a context handler for the block system. @@ -37,19 +38,21 @@ public function __construct(DomainNegotiatorInterface $negotiator) { * {@inheritdoc} */ public function getRuntimeContexts(array $unqualified_context_ids) { + $context = NULL; // Load the current domain. $current_domain = $this->negotiator->getActiveDomain(); - // Set the context. - $context = new Context(new ContextDefinition('entity:domain', $this->t('Active domain')), $current_domain); - - // Allow caching. - $cacheability = new CacheableMetadata(); - $cacheability->setCacheContexts(['url.site']); - $context->addCacheableDependency($cacheability); + // Set the context, if we have a domain. + if (!empty($current_domain) && !empty($current_domain->id())) { + $context = EntityContext::fromEntity($current_domain, $this->t('Active domain')); + // Allow caching. + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['url.site']); + $context->addCacheableDependency($cacheability); + } // Prepare the result. $result = [ - 'entity:domain' => $context, + 'domain' => $context, ]; return $result; @@ -59,7 +62,11 @@ public function getRuntimeContexts(array $unqualified_context_ids) { * {@inheritdoc} */ public function getAvailableContexts() { - return $this->getRuntimeContexts([]); + // See https://www.drupal.org/project/domain/issues/3201514 + if ($this->negotiator->getActiveDomain()) { + return $this->getRuntimeContexts([]); + } + return []; } } diff --git a/domain/src/Controller/DomainController.php b/domain/src/Controller/DomainController.php index c245e697..446cc965 100644 --- a/domain/src/Controller/DomainController.php +++ b/domain/src/Controller/DomainController.php @@ -19,7 +19,7 @@ class DomainController { * * @param \Drupal\domain\DomainInterface $domain * A domain record object. - * @param string|NULL $op + * @param string|null $op * The operation being performed, either 'default' to make the domain record * the default, 'enable' to enable the domain record, or 'disable' to * disable the domain record. @@ -62,14 +62,14 @@ public function ajaxOperation(DomainInterface $domain, $op = NULL) { // Set a message. if ($success) { - drupal_set_message($message); + \Drupal::messenger()->addMessage($message); } else { - drupal_set_message($this->t('The operation failed.')); + \Drupal::messenger()->addError($this->t('The operation failed.')); } // Return to the invoking page. - $url = Url::fromRoute('domain.admin', array(), array('absolute' => TRUE)); + $url = Url::fromRoute('domain.admin', [], ['absolute' => TRUE]); return new RedirectResponse($url->toString(), 302); } diff --git a/domain/src/Controller/DomainControllerBase.php b/domain/src/Controller/DomainControllerBase.php index f979778a..71a855dd 100644 --- a/domain/src/Controller/DomainControllerBase.php +++ b/domain/src/Controller/DomainControllerBase.php @@ -3,9 +3,9 @@ namespace Drupal\domain\Controller; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\domain\DomainStorageInterface; /** * Sets a base class for injecting domain information into controllers. @@ -21,38 +21,37 @@ class DomainControllerBase extends ControllerBase { /** * The entity storage. * - * @var \Drupal\Core\Config\Entity\ConfigEntityStorage + * @var \Drupal\domain\DomainStorageInterface */ - protected $entityStorage; + protected $domainStorage; /** * The entity manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $entityManager; + protected $entityTypeManager; /** * Constructs a new DomainControllerBase. * - * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage + * @param \Drupal\domain\DomainStorageInterface $domain_storage * The storage controller. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity manager. */ - public function __construct(EntityStorageInterface $entity_storage, EntityTypeManagerInterface $entity_manager) { - $this->entityStorage = $entity_storage; - $this->entityManager = $entity_manager; + public function __construct(DomainStorageInterface $domain_storage, EntityTypeManagerInterface $entity_type_manager) { + $this->domainStorage = $domain_storage; + $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - $entity_manager = $container->get('entity_type.manager'); return new static( - $entity_manager->getStorage('domain'), - $entity_manager + $container->get('entity_type.manager')->getStorage('domain'), + $container->get('entity_type.manager') ); } diff --git a/domain/src/DomainAccessControlHandler.php b/domain/src/DomainAccessControlHandler.php index 10284508..ea566734 100644 --- a/domain/src/DomainAccessControlHandler.php +++ b/domain/src/DomainAccessControlHandler.php @@ -9,7 +9,8 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\domain\DomainElementManagerInterface; +use Drupal\domain\DomainInterface; +use Drupal\user\UserStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,11 +20,11 @@ */ class DomainAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface { - /** - * The entity type manager - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ protected $entityTypeManager; /** @@ -35,6 +36,8 @@ class DomainAccessControlHandler extends EntityAccessControlHandler implements E /** * The user storage manager. + * + * @var \Drupal\user\UserStorageInterface */ protected $userStorage; @@ -47,12 +50,14 @@ class DomainAccessControlHandler extends EntityAccessControlHandler implements E * The entity type manager. * @param \Drupal\domain\DomainElementManagerInterface $domain_element_manager * The domain field element manager. + * @param \Drupal\user\UserStorageInterface $user_storage + * The user storage manager. */ - public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, DomainElementManagerInterface $domain_element_manager) { + public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, DomainElementManagerInterface $domain_element_manager, UserStorageInterface $user_storage) { parent::__construct($entity_type); $this->entityTypeManager = $entity_type_manager; $this->domainElementManager = $domain_element_manager; - $this->userStorage = $this->entityTypeManager->getStorage('user'); + $this->userStorage = $user_storage; } /** @@ -62,7 +67,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI return new static( $entity_type, $container->get('entity_type.manager'), - $container->get('domain.element_manager') + $container->get('domain.element_manager'), + $container->get('entity_type.manager')->getStorage('user') ); } @@ -75,10 +81,6 @@ public function checkAccess(EntityInterface $entity, $operation, AccountInterfac if ($account->hasPermission('administer domains')) { return AccessResult::allowed(); } - // @TODO: This may not be relevant. - if ($operation == 'create' && $account->hasPermission('create domains')) { - return AccessResult::allowed(); - } // For view, we allow admins unless the domain is inactive. $is_admin = $this->isDomainAdmin($entity, $account); if ($operation == 'view' && ($entity->status() || $account->hasPermission('access inactive domains')) && ($is_admin || $account->hasPermission('view domain list'))) { @@ -94,19 +96,30 @@ public function checkAccess(EntityInterface $entity, $operation, AccountInterfac return AccessResult::forbidden(); } + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + if ($account->hasPermission('administer domains') || $account->hasPermission('create domains')) { + return AccessResult::allowed(); + } + return AccessResult::neutral(); + } + /** * Checks if a user can administer a specific domain. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to retrieve field data from. - * @param \Drupal\Core\Session\AccountInterface + * @param \Drupal\Core\Session\AccountInterface $account * The user account. * - * @return boolean + * @return bool + * TRUE if a user can administer a specific domain, or FALSE. */ public function isDomainAdmin(EntityInterface $entity, AccountInterface $account) { $user = $this->userStorage->load($account->id()); - $user_domains = $this->domainElementManager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $user_domains = $this->domainElementManager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); return isset($user_domains[$entity->id()]); } diff --git a/domain/src/DomainCreator.php b/domain/src/DomainCreator.php deleted file mode 100644 index d17b67cf..00000000 --- a/domain/src/DomainCreator.php +++ /dev/null @@ -1,78 +0,0 @@ -loader = $loader; - $this->negotiator = $negotiator; - } - - /** - * {@inheritdoc} - */ - public function createDomain(array $values = array()) { - $default = $this->loader->loadDefaultId(); - $domains = $this->loader->loadMultiple(); - if (empty($values)) { - $values['hostname'] = $this->createHostname(); - $values['name'] = \Drupal::config('system.site')->get('name'); - $values['id'] = $this->createMachineName($values['hostname']); - } - $values += array( - 'scheme' => empty($GLOBALS['is_https']) ? 'http' : 'https', - 'status' => 1, - 'weight' => count($domains) + 1, - 'is_default' => (int) empty($default), - ); - $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); - - return $domain; - } - - /** - * {@inheritdoc} - */ - public function createHostname() { - return $this->negotiator->negotiateActiveHostname(); - } - - /** - * {@inheritdoc} - */ - public function createMachineName($hostname = NULL) { - if (empty($hostname)) { - $hostname = $this->createHostname(); - } - return preg_replace('/[^a-z0-9_]/', '_', $hostname); - } - -} diff --git a/domain/src/DomainCreatorInterface.php b/domain/src/DomainCreatorInterface.php deleted file mode 100644 index 51635113..00000000 --- a/domain/src/DomainCreatorInterface.php +++ /dev/null @@ -1,45 +0,0 @@ -loader = $loader; + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + $this->domainStorage = $entity_type_manager->getStorage('domain'); } /** - * @inheritdoc + * {@inheritdoc} */ public function setFormOptions(array $form, FormStateInterface $form_state, $field_name, $hide_on_disallow = FALSE) { // There are cases, such as Entity Browser, where the form is partially @@ -46,29 +59,49 @@ public function setFormOptions(array $form, FormStateInterface $form_state, $fie return $form; } $fields = $this->fieldList($field_name); + $empty = FALSE; $disallowed = $this->disallowedOptions($form_state, $form[$field_name]); - $empty = empty($form[$field_name]['widget']['#options']); + if (empty($form[$field_name]['widget']['#options']) || + (count($form[$field_name]['widget']['#options']) == 1 && + isset($form[$field_name]['widget']['#options']['_none']) + ) + ) { + $empty = TRUE; + } + // If the domain form element is set as a group, and the field is not + // assigned to another group, then move it. See + // domain_access_form_node_form_alter(). + if (isset($form['domain']) && !isset($form[$field_name]['#group'])) { + $form[$field_name]['#group'] = 'domain'; + } + // If no values and we should hide the element, do so. + if ($hide_on_disallow && $empty) { + $form[$field_name]['#access'] = FALSE; + } // Check for domains the user cannot access or the absence of any options. if (!empty($disallowed) || $empty) { // @TODO: Potentially show this information to users with permission. - $form[$field_name . '_disallowed'] = array( + $form[$field_name . '_disallowed'] = [ '#type' => 'value', '#value' => $disallowed, - ); - $form['domain_hidden_fields'] = array( + ]; + $form['domain_hidden_fields'] = [ '#type' => 'value', '#value' => $fields, - ); + ]; if ($hide_on_disallow || $empty) { $form[$field_name]['#access'] = FALSE; } + elseif (!empty($disallowed)) { + $form[$field_name]['widget']['#description'] .= $this->listDisallowed($disallowed); + } // Call our submit function to merge in values. // Account for all the submit buttons on the node form. $buttons = ['preview', 'delete']; $submit = $this->getSubmitHandler(); foreach ($form['actions'] as $key => $action) { - if (!in_array($key, $buttons) && is_array($action) && !in_array($submit, $form['actions'][$key]['#submit'])) { + if (!in_array($key, $buttons) && isset($form['actions'][$key]['#submit']) && !in_array($submit, $form['actions'][$key]['#submit'])) { array_unshift($form['actions'][$key]['#submit'], $submit); } } @@ -78,19 +111,25 @@ public function setFormOptions(array $form, FormStateInterface $form_state, $fie } /** - * @inheritdoc + * {@inheritdoc} */ public static function submitEntityForm(array &$form, FormStateInterface $form_state) { $fields = $form_state->getValue('domain_hidden_fields'); foreach ($fields as $field) { + $entity_values = []; $values = $form_state->getValue($field . '_disallowed'); if (!empty($values)) { $info = $form_state->getBuildInfo(); $node = $form_state->getFormObject()->getEntity(); $entity_values = $form_state->getValue($field); } - foreach ($values as $value) { - $entity_values[]['target_id'] = $value; + if (is_array($values)) { + foreach ($values as $value) { + $entity_values[]['target_id'] = $value; + } + } + else { + $entity_values[]['target_id'] = $values; } // Prevent a fatal error caused by passing a NULL value. // See https://www.drupal.org/node/2841962. @@ -101,9 +140,9 @@ public static function submitEntityForm(array &$form, FormStateInterface $form_s } /** - * @inheritdoc + * {@inheritdoc} */ - public function disallowedOptions(FormStateInterface $form_state, $field) { + public function disallowedOptions(FormStateInterface $form_state, array $field) { $options = []; $info = $form_state->getBuildInfo(); $entity = $form_state->getFormObject()->getEntity(); @@ -115,31 +154,33 @@ public function disallowedOptions(FormStateInterface $form_state, $field) { } /** - * @inheritdoc + * {@inheritdoc} */ public function fieldList($field_name) { static $fields = []; $fields[] = $field_name; - return $fields; + // Return only unique field names. AJAX requests can result in duplicates. + // See https://www.drupal.org/project/domain/issues/2930934. + return array_unique($fields); } /** - * @inheritdoc + * {@inheritdoc} */ - public function getFieldValues($entity, $field_name) { + public function getFieldValues(EntityInterface $entity, $field_name) { // @TODO: static cache. - $list = array(); + $list = []; // @TODO In tests, $entity is returning NULL. if (is_null($entity)) { return $list; } // Get the values of an entity. - $values = $entity->get($field_name); + $values = $entity->hasField($field_name) ? $entity->get($field_name) : NULL; // Must be at least one item. if (!empty($values)) { foreach ($values as $item) { if ($target = $item->getValue()) { - if ($domain = $this->loader->load($target['target_id'])) { + if ($domain = $this->domainStorage->load($target['target_id'])) { $list[$domain->id()] = $domain->getDomainId(); } } @@ -149,10 +190,33 @@ public function getFieldValues($entity, $field_name) { } /** - * @inheritdoc + * {@inheritdoc} */ public function getSubmitHandler() { return '\\Drupal\\domain\\DomainElementManager::submitEntityForm'; } + /** + * Lists the disallowed domains in the user interface. + * + * @param array $disallowed + * An array of domain ids. + * + * @return string + * A string suitable for display. + */ + public function listDisallowed(array $disallowed) { + $domains = $this->domainStorage->loadMultiple($disallowed); + $string = $this->t('The following domains are currently assigned and cannot be changed:'); + foreach ($domains as $domain) { + $items[] = $domain->label(); + } + $build = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + $string .= render($build); + return '
' . $string . '
'; + } + } diff --git a/domain/src/DomainElementManagerInterface.php b/domain/src/DomainElementManagerInterface.php index 31c8e47d..9c6832dc 100644 --- a/domain/src/DomainElementManagerInterface.php +++ b/domain/src/DomainElementManagerInterface.php @@ -2,6 +2,7 @@ namespace Drupal\domain; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; /** @@ -22,16 +23,16 @@ interface DomainElementManagerInterface { * * @param array $form * The form array. - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state object. - * @param $field_name + * @param string $field_name * The name of the field to check. - * @param boolean $hide_on_disallow + * @param bool $hide_on_disallow * If the field is set to a value that cannot be altered by the user who * is not assigned to that domain, pass TRUE to remove the form element * entirely. See DomainSourceElementManager for the use-case. * - * @return array $form + * @return array * Return the modified form array. */ public function setFormOptions(array $form, FormStateInterface $form_state, $field_name, $hide_on_disallow = FALSE); @@ -42,30 +43,29 @@ public function setFormOptions(array $form, FormStateInterface $form_state, $fie * On form submit, loop through the hidden form values and add those to the * entity being saved. * - * @param $form + * No return value. Hidden values are added to the field values directly. + * + * @param array $form * The form array. - * @param Drupal\Core\Form\FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state object. - * - * @return - * No return value. Hidden values are added to the field values directly. */ public static function submitEntityForm(array &$form, FormStateInterface $form_state); /** * Finds options not accessible to the current user. * - * @param Drupal\Core\Form\FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state object. * @param array $field * The field element being processed. */ - public function disallowedOptions(FormStateInterface $form_state, $field); + public function disallowedOptions(FormStateInterface $form_state, array $field); /** * Stores a static list of fields that have been disallowed. * - * @param $field_name + * @param string $field_name * The name of the field being processed. Inherited from setFormOptions. * * @return array @@ -85,7 +85,7 @@ public function fieldList($field_name); * The domain access field values, keyed by id (machine_name) with value of * the numeric domain_id used by node access. */ - public function getFieldValues($entity, $field_name); + public function getFieldValues(EntityInterface $entity, $field_name); /** * Returns the default submit handler to be used for a field element. @@ -97,8 +97,8 @@ public function getFieldValues($entity, $field_name); * The method must be public and static, since it will be called from the * form submit handler without knowledge of the parent class. * - * The base implementat is submitEntityForm, and can be overridden by - * specific subclasses. + * The base implementation is submitEntityForm, and can be overridden by + * specific subclasses. */ public function getSubmitHandler(); diff --git a/domain/src/DomainForm.php b/domain/src/DomainForm.php index 65732a5f..2b20c029 100644 --- a/domain/src/DomainForm.php +++ b/domain/src/DomainForm.php @@ -3,13 +3,75 @@ namespace Drupal\domain; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base form for domain edit forms. */ class DomainForm extends EntityForm { + /** + * The domain entity storage. + * + * @var \Drupal\domain\DomainStorageInterface + */ + protected $domainStorage; + + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The domain validator. + * + * @var \Drupal\domain\DomainValidatorInterface + */ + protected $validator; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a DomainForm object. + * + * @param \Drupal\domain\DomainStorageInterface $domain_storage + * The domain storage manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer. + * @param \Drupal\domain\DomainValidatorInterface $validator + * The domain validator. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(DomainStorageInterface $domain_storage, RendererInterface $renderer, DomainValidatorInterface $validator, EntityTypeManagerInterface $entity_type_manager) { + $this->domainStorage = $domain_storage; + $this->renderer = $renderer; + $this->validator = $validator; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('domain'), + $container->get('renderer'), + $container->get('domain.validator'), + $container->get('entity_type.manager') + ); + } + /** * {@inheritdoc} */ @@ -17,70 +79,82 @@ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); /** @var \Drupal\domain\Entity\Domain $domain */ $domain = $this->entity; - $domains = \Drupal::service('domain.loader')->loadMultiple(); + // Create defaults if this is the first domain. - if (empty($domains)) { - $domain->addProperty('hostname', \Drupal::service('domain.creator')->createHostname()); - $domain->addProperty('name', \Drupal::config('system.site')->get('name')); + $count_existing = $this->domainStorage->getQuery()->count()->execute(); + if (!$count_existing) { + $domain->addProperty('hostname', $this->domainStorage->createHostname()); + $domain->addProperty('name', $this->config('system.site')->get('name')); } - $form['domain_id'] = array( + $form['domain_id'] = [ '#type' => 'value', '#value' => $domain->getDomainId(), - ); - $form['hostname'] = array( + ]; + $form['hostname'] = [ '#type' => 'textfield', '#title' => $this->t('Hostname'), '#size' => 40, '#maxlength' => 80, - '#default_value' => $domain->getHostname(), + '#default_value' => $domain->getCanonical(), '#description' => $this->t('The canonical hostname, using the full subdomain.example.com format. Leave off the http:// and the trailing slash and do not include any paths.
If this domain uses a custom http(s) port, you should specify it here, e.g.: subdomain.example.com:1234
The hostname may contain only lowercase alphanumeric characters, dots, dashes, and a colon (if using alternative ports).'), - ); - $form['id'] = array( + ]; + $form['id'] = [ '#type' => 'machine_name', - '#default_value' => $domain->id(), - '#machine_name' => array( - 'source' => array('hostname'), - 'exists' => '\Drupal\domain\Entity\Domain::load', - ), - ); - $form['name'] = array( + '#default_value' => !empty($domain->id()) ? $domain->id() : '', + '#disabled' => !empty($domain->id()), + '#machine_name' => [ + 'source' => ['hostname'], + 'exists' => [$this->domainStorage, 'load'], + ], + ]; + $form['name'] = [ '#type' => 'textfield', '#title' => $this->t('Name'), '#size' => 40, '#maxlength' => 80, '#default_value' => $domain->label(), '#description' => $this->t('The human-readable name is shown in domain lists and may be used as the title tag.'), - ); + ]; // Do not use the :// suffix when storing data. $add_suffix = FALSE; - $form['scheme'] = array( + $form['scheme'] = [ '#type' => 'radios', '#title' => $this->t('Domain URL scheme'), - '#options' => array('http' => 'http://', 'https' => 'https://'), - '#default_value' => $domain->getScheme($add_suffix), - '#description' => $this->t('This URL scheme will be used when writing links and redirects to this domain and its resources.'), - ); - $form['status'] = array( + '#options' => [ + 'http' => 'http://', + 'https' => 'https://', + 'variable' => 'Variable', + ], + '#default_value' => $domain->getRawScheme(), + '#description' => $this->t('This URL scheme will be used when writing links and redirects to this domain and its resources. Selecting Variable will inherit the current scheme of the web request.'), + ]; + $form['status'] = [ '#type' => 'radios', '#title' => $this->t('Domain status'), - '#options' => array(1 => $this->t('Active'), 0 => $this->t('Inactive')), + '#options' => [1 => $this->t('Active'), 0 => $this->t('Inactive')], '#default_value' => (int) $domain->status(), '#description' => $this->t('"Inactive" domains are only accessible to user roles with that assigned permission.'), - ); - $form['weight'] = array( + ]; + $form['weight'] = [ '#type' => 'weight', '#title' => $this->t('Weight'), - '#delta' => count(\Drupal::service('domain.loader')->loadMultiple()) + 1, + '#delta' => $count_existing + 1, '#default_value' => $domain->getWeight(), '#description' => $this->t('The sort order for this record. Lower values display first.'), - ); - $form['is_default'] = array( + ]; + $form['is_default'] = [ '#type' => 'checkbox', '#title' => $this->t('Default domain'), '#default_value' => $domain->isDefault(), '#description' => $this->t('If a URL request fails to match a domain record, the settings for this domain will be used. Only one domain can be default.'), - ); - $required = \Drupal::service('domain.validator')->getRequiredFields(); + ]; + $form['validate_url'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Test server response'), + '#default_value' => TRUE, + '#description' => $this->t('Validate that url of the host is accessible to Drupal before saving.'), + ]; + $required = $this->validator->getRequiredFields(); foreach ($form as $key => $element) { if (in_array($key, $required)) { $form[$key]['#required'] = TRUE; @@ -92,12 +166,47 @@ public function form(array $form, FormStateInterface $form_state) { /** * {@inheritdoc} */ - public function validate(array $form, FormStateInterface $form_state) { - $entity = $this->buildEntity($form, $form_state); - $validator = \Drupal::service('domain.validator'); - $errors = $validator->validate($entity); + public function validateForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\domain\DomainInterface $entity */ + $entity = $this->entity; + $hostname = $entity->getHostname(); + $errors = $this->validator->validate($hostname); if (!empty($errors)) { - $form_state->setErrorByName('hostname', $errors); + // Render errors to display as message. + $message = [ + '#theme' => 'item_list', + '#items' => $errors, + ]; + $message = $this->renderer->renderPlain($message); + $form_state->setErrorByName('hostname', $message); + } + // Validate if the same hostname exists. + // Do not use domain loader because it may change hostname. + $existing = $this->domainStorage->loadByProperties(['hostname' => $hostname]); + $existing = reset($existing); + // If we have already registered a hostname, make sure we don't create a + // duplicate. + // We cannot check id() here, as the machine name is editable. + if ($existing && $existing->getDomainId() != $entity->getDomainId()) { + $form_state->setErrorByName('hostname', $this->t('The hostname is already registered.')); + } + + // Is validate_url set? + if ($entity->validate_url) { + // Check the domain response. First, clear the path value. + $entity->setPath(); + // Check the response. + $response = $this->validator->checkResponse($entity); + // If validate_url is set, then we must receive a 200 response. + if ($response !== 200) { + if (empty($response)) { + $response = 500; + } + $form_state->setErrorByName('hostname', $this->t('The server request to @url returned a @response response. To proceed, disable the Test server response in the form.', [ + '@url' => $entity->getPath(), + '@response' => $response + ])); + } } } @@ -105,23 +214,13 @@ public function validate(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { - $domain = $this->entity; - if ($domain->isNew()) { - drupal_set_message($this->t('Domain record created.')); + $status = parent::save($form, $form_state); + if ($status == SAVED_NEW) { + \Drupal::messenger()->addMessage($this->t('Domain record created.')); } else { - drupal_set_message($this->t('Domain record updated.')); + \Drupal::messenger()->addMessage($this->t('Domain record updated.')); } - $domain->save(); - $form_state->setRedirect('domain.admin'); - } - - /** - * {@inheritdoc} - */ - public function delete(array &$form, FormStateInterface $form_state) { - $domain = $this->entity; - $domain->delete(); $form_state->setRedirect('domain.admin'); } diff --git a/domain/src/DomainInterface.php b/domain/src/DomainInterface.php index cc774d63..9fa17c22 100644 --- a/domain/src/DomainInterface.php +++ b/domain/src/DomainInterface.php @@ -3,16 +3,23 @@ namespace Drupal\domain; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\domain\DomainNegotiatorInterface; /** * Provides an interface defining a domain entity. */ interface DomainInterface extends ConfigEntityInterface { + /** + * The name of the admin access control field. + */ + const DOMAIN_ADMIN_FIELD = 'field_domain_admin'; + /** * Detects if the current domain is the active domain. * * @return bool + * TRUE if domain enabled, FALSE otherwise. */ public function isActive(); @@ -20,6 +27,7 @@ public function isActive(); * Detects if the current domain is the default domain. * * @return bool + * TRUE if domain set as default, FALSE otherwise. */ public function isDefault(); @@ -27,6 +35,7 @@ public function isDefault(); * Detects if the domain uses https for links. * * @return bool + * TRUE if domain protocol is HTTPS, FALSE otherwise. */ public function isHttps(); @@ -74,7 +83,9 @@ public function getPath(); public function getUrl(); /** - * Returns the scheme for a domain record. + * Returns the active scheme for a domain record. + * + * This method is to be used when generating URLs. * * @param bool $add_suffix * Tells the method to return :// after the string. @@ -84,6 +95,17 @@ public function getUrl(); */ public function getScheme($add_suffix = TRUE); + /** + * Returns the stored scheme value for a domain record. + * + * This method is to be used with forms and when saving domain records. It + * returns the raw value (http|https|variable) of the domain's default scheme. + * + * @return string + * Returns a stored scheme default (http|https|variable) for the record. + */ + public function getRawScheme(); + /** * Retrieves the value of the response test. * @@ -124,7 +146,7 @@ public function getLink($current_path = TRUE); /** * Returns the redirect status of the current domain. * - * @return integer | NULL + * @return int|null * If numeric, the type of redirect to issue (301 or 302). */ public function getRedirect(); @@ -175,17 +197,17 @@ public function getWeight(); * @param int $match_type * A numeric constant indicating the type of match derived by the caller. * Use this value to determine if the request needs to be overridden. Valid - * types are DomainNegotiator::DOMAIN_MATCH_NONE, - * DomainNegotiator::DOMAIN_MATCH_EXACT, - * DomainNegotiator::DOMAIN_MATCH_ALIAS. + * types are DomainNegotiatorInterface::DOMAIN_MATCH_NONE, + * DomainNegotiatorInterface::DOMAIN_MATCH_EXACT, + * DomainNegotiatorInterface::DOMAIN_MATCH_ALIAS. */ - public function setMatchType($match_type = DomainNegotiator::DOMAIN_MATCH_EXACT); + public function setMatchType($match_type = DomainNegotiatorInterface::DOMAIN_MATCHED_EXACT); /** * Gets the type of record match returned by the negotiator. * * This value will be set by the domain negotiation routine and is not present - * when loading a domain record via DomainLoaderInterface. + * when loading a domain record via DomainStorageInterface. * * @return int * The domain record match type. @@ -194,9 +216,30 @@ public function setMatchType($match_type = DomainNegotiator::DOMAIN_MATCH_EXACT) */ public function getMatchType(); + /** + * Find the port used for the domain. + * + * @return string + * An optional port string (e.g. ':8080') or an empty string; + */ + public function getPort(); + /** * Creates a unique domain id for this record. */ public function createDomainId(); + /** + * Retrieves the canonical (registered) hostname for the domain. + * + * @return string + * A hostname string. + */ + public function getCanonical(); + + /** + * Sets the canonical (registered) hostname for the domain. + */ + public function setCanonical($hostname = NULL); + } diff --git a/domain/src/DomainListBuilder.php b/domain/src/DomainListBuilder.php index c6448d9a..787dda0a 100644 --- a/domain/src/DomainListBuilder.php +++ b/domain/src/DomainListBuilder.php @@ -6,14 +6,13 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Routing\RedirectDestinationInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; -use Drupal\domain\DomainAccessControlHandler; -use Drupal\domain\DomainLoaderInterface; +use Drupal\domain\DomainInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -62,11 +61,25 @@ class DomainListBuilder extends DraggableListBuilder { protected $moduleHandler; /** - * The domain loader. + * The Domain storage handler. * - * @var \Drupal\domain\DomainLoaderInterface + * @var \Drupal\domain\DomainStorageInterface */ - protected $domainLoader; + protected $domainStorage; + + /** + * The domain field element manager. + * + * @var \Drupal\domain\DomainElementManagerInterface + */ + protected $domainElementManager; + + /** + * The User storage handler. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; /** * {@inheritdoc} @@ -74,44 +87,47 @@ class DomainListBuilder extends DraggableListBuilder { public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, - $container->get('entity.manager')->getStorage($entity_type->id()), + $container->get('entity_type.manager')->getStorage($entity_type->id()), $container->get('current_user'), $container->get('redirect.destination'), $container->get('entity_type.manager'), $container->get('module_handler'), - $container->get('domain.loader') + $container->get('domain.element_manager') ); } /** - * Constructs a new EntityListBuilder object. + * Constructs a new DomainListBuilder object. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type definition. - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The entity storage class. + * @param \Drupal\domain\DomainStorageInterface $domain_storage + * The domain storage class. * @param \Drupal\Core\Session\AccountInterface $account * The active user account. - * @param \Drupal\Core\Routing\RedirectDestinationInterface $destination + * @param \Drupal\Core\Routing\RedirectDestinationInterface $destination_handler * The redirect destination helper. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. - * @param \Drupal\domain\DomainLoaderInterface $domain_loader - * The domain loader. + * @param \Drupal\domain\DomainElementManagerInterface $domain_element_manager + * The domain field element manager. */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, AccountInterface $account, RedirectDestinationInterface $destination_handler, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, DomainLoaderInterface $domain_loader) { - parent::__construct($entity_type, $storage); + public function __construct(EntityTypeInterface $entity_type, DomainStorageInterface $domain_storage, AccountInterface $account, RedirectDestinationInterface $destination_handler, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, DomainElementManagerInterface $domain_element_manager) { + parent::__construct($entity_type, $domain_storage); $this->entityTypeId = $entity_type->id(); - $this->storage = $storage; + $this->domainStorage = $domain_storage; $this->entityType = $entity_type; $this->currentUser = $account; $this->destinationHandler = $destination_handler; $this->entityTypeManager = $entity_type_manager; $this->accessHandler = $this->entityTypeManager->getAccessControlHandler('domain'); $this->moduleHandler = $module_handler; - $this->domainLoader = $domain_loader; + $this->domainElementManager = $domain_element_manager; + $this->userStorage = $this->entityTypeManager->getStorage('user'); + // DraggableListBuilder sets this to FALSE, which cancels any pagination. + $this->limit = 50; } /** @@ -139,42 +155,42 @@ public function getOperations(EntityInterface $entity) { $super_admin = $this->currentUser->hasPermission('administer domains'); if ($super_admin || $this->currentUser->hasPermission('access inactive domains')) { if ($entity->status() && !$default) { - $operations['disable'] = array( + $operations['disable'] = [ 'title' => $this->t('Disable'), - 'url' => Url::fromRoute('domain.inline_action', array('op' => 'disable', 'domain' => $id)), + 'url' => Url::fromRoute('domain.inline_action', ['op' => 'disable', 'domain' => $id]), 'weight' => 50, - ); + ]; } elseif (!$default) { - $operations['enable'] = array( + $operations['enable'] = [ 'title' => $this->t('Enable'), - 'url' => Url::fromRoute('domain.inline_action', array('op' => 'enable', 'domain' => $id)), + 'url' => Url::fromRoute('domain.inline_action', ['op' => 'enable', 'domain' => $id]), 'weight' => 40, - ); + ]; } } if (!$default && $super_admin) { - $operations['default'] = array( + $operations['default'] = [ 'title' => $this->t('Make default'), - 'url' => Url::fromRoute('domain.inline_action', array('op' => 'default', 'domain' => $id)), + 'url' => Url::fromRoute('domain.inline_action', ['op' => 'default', 'domain' => $id]), 'weight' => 30, - ); + ]; } if (!$default && $this->accessHandler->checkAccess($entity, 'delete')->isAllowed()) { - $operations['delete'] = array( + $operations['delete'] = [ 'title' => $this->t('Delete'), - 'url' => Url::fromRoute('entity.domain.delete_form', array('domain' => $id)), + 'url' => Url::fromRoute('entity.domain.delete_form', ['domain' => $id]), 'weight' => 20, - ); + ]; } - $operations += $this->moduleHandler->invokeAll('domain_operations', array($entity, $this->currentUser)); + $operations += $this->moduleHandler->invokeAll('domain_operations', [$entity, $this->currentUser]); foreach ($operations as $key => $value) { if (isset($value['query']['token'])) { $operations[$key]['query'] += $destination; } } /** @var DomainInterface $default */ - $default = $this->domainLoader->loadDefaultDomain(); + $default = $this->domainStorage->loadDefaultDomain(); // Deleting the site default domain is not allowed. if ($default && $id == $default->id()) { @@ -191,6 +207,7 @@ public function buildHeader() { $header['hostname'] = $this->t('Hostname'); $header['status'] = $this->t('Status'); $header['is_default'] = $this->t('Default'); + $header['scheme'] = $this->t('Scheme'); $header += parent::buildHeader(); if (!$this->currentUser->hasPermission('administer domains')) { unset($header['weight']); @@ -208,22 +225,25 @@ public function buildRow(EntityInterface $entity) { return; } - $row['label'] = $this->getLabel($entity); - $row['hostname'] = array('#markup' => $entity->getLink()); + $row['label'] = $entity->label(); + $row['hostname'] = ['#markup' => $entity->getLink()]; if ($entity->isActive()) { $row['hostname']['#prefix'] = ''; $row['hostname']['#suffix'] = ''; } - $row['status'] = array('#markup' => $entity->status() ? $this->t('Active') : $this->t('Inactive')); - $row['is_default'] = array('#markup' => ($entity->isDefault() ? $this->t('Yes') : $this->t('No'))); + $row['status'] = ['#markup' => $entity->status() ? $this->t('Active') : $this->t('Inactive')]; + $row['is_default'] = ['#markup' => ($entity->isDefault() ? $this->t('Yes') : $this->t('No'))]; + $row['scheme'] = ['#markup' => $entity->getRawScheme()]; $row += parent::buildRow($entity); + if ($entity->getRawScheme() === 'variable') { + $row['scheme']['#markup'] .= ' (' . $entity->getScheme(FALSE) . ')'; + } + if (!$this->currentUser->hasPermission('administer domains')) { unset($row['weight']); } - else { - $row['weight']['#delta'] = count($this->domainLoader->loadMultiple()) + 1; - } + return $row; } @@ -239,15 +259,98 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['actions']['submit']['#access'] = FALSE; unset($form['#tabledrag']); } + // Delta is set after each row is loaded. + $count = count($this->domainStorage->loadMultiple()) + 1; + foreach (Element::children($form['domains']) as $key) { + if (isset($form['domains'][$key]['weight'])) { + $form['domains'][$key]['weight']['#delta'] = $count; + } + } return $form; } /** * {@inheritdoc} + * + * Overrides the parent method to prevent saving bad data. + * + * @link https://www.drupal.org/project/domain/issues/2925798 + * @link https://www.drupal.org/project/domain/issues/2925629 */ public function submitForm(array &$form, FormStateInterface $form_state) { - parent::submitForm($form, $form_state); - drupal_set_message($this->t('Configuration saved.')); + foreach ($form_state->getValue($this->entitiesKey) as $id => $value) { + if (isset($this->entities[$id]) && $this->entities[$id]->get($this->weightKey) != $value['weight']) { + // Reset weight properly. + $this->entities[$id]->set($this->weightKey, $value['weight']); + // Do not allow accidental hostname rewrites. + $this->entities[$id]->set('hostname', $this->entities[$id]->getCanonical()); + $this->entities[$id]->save(); + } + } + } + + /** + * Internal sort method for form weights. + */ + private function sortByWeight($a, $b) { + if ($a['weight'] < $b['weight']) { + return 0; + } + return 1; + } + + /** + * {@inheritdoc} + * + * Builds the entity listing as a form with pagination. This method overrides + * both Drupal\Core\Config\Entity\DraggableListBuilder::render() and + * Drupal\Core\Entity\EntityListBuilder::render(). + */ + public function render() { + // Build the default form, which includes weights. + $form = $this->formBuilder()->getForm($this); + + // Only add the pager if a limit is specified. + if ($this->limit) { + $form['pager'] = [ + '#type' => 'pager', + ]; + } + return $form; + } + + /** + * {@inheritdoc} + * + * Loads entity IDs using a pager sorted by the entity weight. The default + * behavior when using a limit is to sort by id. + * + * We also have to limit by assigned domains of the active user. + * + * See Drupal\Core\Entity\EntityListBuilder::getEntityIds() + * + * @return array + * An array of entity IDs. + */ + protected function getEntityIds() { + $query = $this->getStorage()->getQuery() + ->sort($this->entityType->getKey('weight')); + + // If the user cannot administer domains, we must filter the query further + // by assigned IDs. We don't have to check permissions here, because that is + // handled by the route system and buildRow(). There are two permissions + // that allow users to view the entire list. + if (!$this->currentUser->hasPermission('administer domains') && !$this->currentUser->hasPermission('view domain list')) { + $user = $this->userStorage->load($this->currentUser->id()); + $allowed = $this->domainElementManager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); + $query->condition('id', array_keys($allowed), 'IN'); + } + + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + return $query->execute(); } } diff --git a/domain/src/DomainLoader.php b/domain/src/DomainLoader.php deleted file mode 100644 index 6f34649a..00000000 --- a/domain/src/DomainLoader.php +++ /dev/null @@ -1,163 +0,0 @@ -typedConfig = $typed_config; - $this->configFactory = $config_factory; - } - - /** - * {@inheritdoc} - */ - public function loadSchema() { - $fields = $this->typedConfig->getDefinition('domain.record.*'); - return isset($fields['mapping']) ? $fields['mapping'] : array(); - } - - /** - * {@inheritdoc} - */ - public function load($id, $reset = FALSE) { - $controller = $this->getStorage(); - if ($reset) { - $controller->resetCache(array($id)); - } - return $controller->load($id); - } - - /** - * {@inheritdoc} - */ - public function loadDefaultId() { - $result = $this->loadDefaultDomain(); - if (!empty($result)) { - return $result->id(); - } - return NULL; - } - - /** - * {@inheritdoc} - */ - public function loadDefaultDomain() { - $result = $this->getStorage()->loadByProperties(array('is_default' => TRUE)); - if (!empty($result)) { - return current($result); - } - return NULL; - } - - /** - * {@inheritdoc} - */ - public function loadMultiple($ids = NULL, $reset = FALSE) { - $controller = $this->getStorage(); - if ($reset) { - $controller->resetCache($ids); - } - return $controller->loadMultiple($ids); - } - - /** - * {@inheritdoc} - */ - public function loadMultipleSorted($ids = NULL) { - $domains = $this->loadMultiple($ids); - uasort($domains, array($this, 'sort')); - return $domains; - } - - /** - * {@inheritdoc} - */ - public function loadByHostname($hostname) { - $hostname = $this->prepareHostname($hostname); - $result = $this->getStorage()->loadByProperties(array('hostname' => $hostname)); - if (empty($result)) { - return NULL; - } - return current($result); - } - - /** - * {@inheritdoc} - */ - public function loadOptionsList() { - $list = array(); - foreach ($this->loadMultipleSorted() as $id => $domain) { - $list[$id] = $domain->label(); - } - return $list; - } - - /** - * {@inheritdoc} - */ - public function sort(DomainInterface $a, DomainInterface $b) { - return $a->getWeight() > $b->getWeight(); - } - - /** - * Loads the storage controller. - * - * We use the loader very early in the request cycle. As a result, if we try - * to inject the storage container, we hit a circular dependency. Using this - * method at least keeps our code easier to update. - */ - protected function getStorage() { - $storage = \Drupal::entityTypeManager()->getStorage('domain'); - return $storage; - } - - /** - * Removes www. from a hostname, if set. - * - * @param string $hostname - * A hostname. - * @return string - */ - public function prepareHostname($hostname) { - // Strip www. off the front? - $www = $this->configFactory->get('domain.settings')->get('www_prefix'); - if (!empty($www) && substr($hostname, 0, 4) == 'www.') { - $hostname = substr($hostname, 4); - } - return $hostname; - } - -} diff --git a/domain/src/DomainNegotiator.php b/domain/src/DomainNegotiator.php index 3200a7a2..3d5fe9ea 100644 --- a/domain/src/DomainNegotiator.php +++ b/domain/src/DomainNegotiator.php @@ -3,6 +3,7 @@ namespace Drupal\domain; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -15,6 +16,10 @@ class DomainNegotiator implements DomainNegotiatorInterface { * Defines record matching types when dealing with request alteration. * * @see hook_domain_request_alter(). + * + * @deprecated These constant will be replaced in the final release by + * Drupal\domain\DomainNegotiatorInterface. Note that the new versions have + * been renamed to maintain beta compatibility. */ const DOMAIN_MATCH_NONE = 0; const DOMAIN_MATCH_EXACT = 1; @@ -22,6 +27,8 @@ class DomainNegotiator implements DomainNegotiatorInterface { /** * The HTTP_HOST value of the request. + * + * @var string */ protected $httpHost; @@ -33,11 +40,18 @@ class DomainNegotiator implements DomainNegotiatorInterface { protected $domain; /** - * The loader class. + * The domain storage class. * - * @var \Drupal\domain\DomainLoaderInterface + * @var \Drupal\domain\DomainStorageInterface|null */ - protected $domainLoader; + protected $domainStorage; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; /** * The request stack object. @@ -68,15 +82,15 @@ class DomainNegotiator implements DomainNegotiatorInterface { * The request stack object. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. - * @param \Drupal\domain\DomainLoaderInterface $loader - * The Domain loader object. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. */ - public function __construct(RequestStack $requestStack, ModuleHandlerInterface $module_handler, DomainLoaderInterface $loader, ConfigFactoryInterface $config_factory) { + public function __construct(RequestStack $requestStack, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory) { $this->requestStack = $requestStack; $this->moduleHandler = $module_handler; - $this->domainLoader = $loader; + $this->entityTypeManager = $entity_type_manager; $this->configFactory = $config_factory; } @@ -87,18 +101,19 @@ public function setRequestDomain($httpHost, $reset = FALSE) { // @TODO: Investigate caching methods. $this->setHttpHost($httpHost); // Try to load a direct match. - if ($domain = $this->domainLoader->loadByHostname($httpHost)) { + if ($domain = $this->domainStorage()->loadByHostname($httpHost)) { // If the load worked, set an exact match flag for the hook. - $domain->setMatchType(self::DOMAIN_MATCH_EXACT); + $domain->setMatchType(DomainNegotiatorInterface::DOMAIN_MATCHED_EXACT); } // If a straight load fails, create a base domain for checking. This data // is required for hook_domain_request_alter(). else { - $values = array('hostname' => $httpHost); - /** @var \Drupal\domain\Entity\DomainInterface $domain */ - $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); - $domain->setMatchType(self::DOMAIN_MATCH_NONE); + $values = ['hostname' => $httpHost]; + /** @var \Drupal\domain\DomainInterface $domain */ + $domain = $this->domainStorage()->create($values); + $domain->setMatchType(DomainNegotiatorInterface::DOMAIN_MATCHED_NONE); } + // Now check with modules (like Domain Alias) that register alternate // lookup systems with the main module. $this->moduleHandler->alter('domain_request', $domain); @@ -108,9 +123,9 @@ public function setRequestDomain($httpHost, $reset = FALSE) { $this->setActiveDomain($domain); } // Fallback to default domain if no match. - elseif ($domain = $this->domainLoader->loadDefaultDomain()) { + elseif ($domain = $this->domainStorage()->loadDefaultDomain()) { $this->moduleHandler->alter('domain_request', $domain); - $domain->setMatchType(self::DOMAIN_MATCH_NONE); + $domain->setMatchType(DomainNegotiatorInterface::DOMAIN_MATCHED_NONE); if (!empty($domain->id())) { $this->setActiveDomain($domain); } @@ -159,10 +174,10 @@ public function negotiateActiveHostname() { $httpHost = $request->getHttpHost(); } else { - $httpHost = $_SERVER['HTTP_HOST']; + $httpHost = $_SERVER['HTTP_HOST'] ?? NULL; } $hostname = !empty($httpHost) ? $httpHost : 'localhost'; - return $this->domainLoader->prepareHostname($hostname); + return $this->domainStorage()->prepareHostname($hostname); } /** @@ -179,4 +194,42 @@ public function getHttpHost() { return $this->httpHost; } + /** + * {@inheritdoc} + */ + public function isRegisteredDomain($hostname) { + // Direct hostname match always passes. + if ($domain = $this->domainStorage()->loadByHostname($hostname)) { + return TRUE; + } + // Check for registered alias matches. + $values = ['hostname' => $hostname]; + /** @var \Drupal\domain\DomainInterface $domain */ + $domain = $this->domainStorage()->create($values); + $domain->setMatchType(DomainNegotiatorInterface::DOMAIN_MATCHED_NONE); + + // Now check with modules (like Domain Alias) that register alternate + // lookup systems with the main module. + $this->moduleHandler->alter('domain_request', $domain); + + // We must have registered a valid id, else the request made no match. + if (!empty($domain->id())) { + return TRUE; + } + return FALSE; + } + + /** + * Retrieves the domain storage handler. + * + * @return \Drupal\domain\DomainStorageInterface + * The domain storage handler. + */ + protected function domainStorage() { + if (!$this->domainStorage) { + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); + } + return $this->domainStorage; + } + } diff --git a/domain/src/DomainNegotiatorInterface.php b/domain/src/DomainNegotiatorInterface.php index 4c7247e7..1a054e39 100644 --- a/domain/src/DomainNegotiatorInterface.php +++ b/domain/src/DomainNegotiatorInterface.php @@ -7,6 +7,29 @@ */ interface DomainNegotiatorInterface { + /** + * Defines record matching types when dealing with request alteration. + * + * These constants are designed to help modules know how to react to a + * domain record match, since an exact match is more important than a pattern + * match. + * + * @see hook_domain_request_alter(). + * + * No matching record found. + */ + const DOMAIN_MATCHED_NONE = 0; + + /** + * An exact domain record string match found. + */ + const DOMAIN_MATCHED_EXACT = 1; + + /** + * An alias pattern match found. + */ + const DOMAIN_MATCHED_ALIAS = 2; + /** * Determines the active domain request. * @@ -78,4 +101,16 @@ public function negotiateActiveHostname(); */ public function getActiveDomain($reset = FALSE); + /** + * Checks that a URL's hostname is registered as a valid domain or alias. + * + * @param string $hostname + * A string representing the hostname of the request (e.g. example.com). + * + * @return bool + * TRUE if a URL's hostname is registered as a valid domain or alias, or + * FALSE. + */ + public function isRegisteredDomain($hostname); + } diff --git a/domain/src/DomainRedirectResponse.php b/domain/src/DomainRedirectResponse.php new file mode 100644 index 00000000..860412ca --- /dev/null +++ b/domain/src/DomainRedirectResponse.php @@ -0,0 +1,179 @@ +getRequestContext()->getCompleteBaseUrl(); + return !UrlHelper::isExternal($url) || UrlHelper::externalIsLocal($url, $base_url) || $this->externalIsRegistered($url, $base_url); + } + + /** + * {@inheritdoc} + */ + protected function isSafe($url) { + return $this->isLocal($url); + } + + /** + * Returns the request context. + * + * @return \Drupal\Core\Routing\RequestContext + * The request context. + */ + protected function getRequestContext() { + if (!isset($this->requestContext)) { + $this->requestContext = \Drupal::service('router.request_context'); + } + return $this->requestContext; + } + + /** + * Sets the request context. + * + * @param \Drupal\Core\Routing\RequestContext $request_context + * The request context. + * + * @return $this + */ + public function setRequestContext(RequestContext $request_context) { + $this->requestContext = $request_context; + + return $this; + } + + /** + * Determines if an external URL points to this domain-aware installation. + * + * This method replaces the logic in + * Drupal\Component\Utility\UrlHelper::externalIsLocal(). Since that class is + * not directly extendable, we have to replace it. + * + * @param string $url + * A string containing an external URL, such as "http://example.com/foo". + * @param string $base_url + * The base URL string to check against, such as "http://example.com/". + * + * @return bool + * TRUE if the URL has the same domain and base path. + * + * @throws \InvalidArgumentException + * Exception thrown when $url is not fully qualified. + */ + public static function externalIsRegistered($url, $base_url) { + $url_parts = parse_url($url); + $base_parts = parse_url($base_url); + + if (empty($url_parts['host'])) { + throw new \InvalidArgumentException('A path was passed when a fully qualified domain was expected.'); + } + + // Check that the host name is registered with trusted hosts. + $trusted = self::checkTrustedHost($url_parts['host']); + if (!$trusted) { + return FALSE; + } + + // Check that the requested $url is registered. + $negotiator = \Drupal::service('domain.negotiator'); + $registered_domain = $negotiator->isRegisteredDomain($url_parts['host']); + + if (!isset($url_parts['path']) || !isset($base_parts['path'])) { + return $registered_domain; + } + else { + // When comparing base paths, we need a trailing slash to make sure a + // partial URL match isn't occurring. Since base_path() always returns + // with a trailing slash, we don't need to add the trailing slash here. + return ($registered_domain && stripos($url_parts['path'], $base_parts['path']) === 0); + } + } + + /** + * Checks that a host is registered with trusted_host_patterns. + * + * This method is cribbed from Symfony's Request::getHost() method. + * + * @param string $host + * The hostname to check. + * + * @return bool + * TRUE if the hostname matches the trusted_host_patterns. FALSE otherwise. + * It is the caller's responsibility to deal with this result securely. + */ + public static function checkTrustedHost($host) { + // See Request::setTrustedHosts(); + if (!isset(self::$trustedHostPatterns)) { + self::$trustedHostPatterns = array_map(function ($hostPattern) { + return sprintf('#%s#i', $hostPattern); + }, Settings::get('trusted_host_patterns', [])); + // Reset the trusted host match array. + self::$trustedHosts = []; + } + + // Trim and remove port number from host. Host is lowercase as per RFC + // 952/2181. + $host = mb_strtolower(preg_replace('/:\d+$/', '', trim($host))); + + // In the original Symfony code, hostname validation runs here. We have + // removed that portion because Domains are already validated on creation. + if (count(self::$trustedHostPatterns) > 0) { + // To avoid host header injection attacks, you should provide a list of + // trusted host patterns. + if (in_array($host, self::$trustedHosts)) { + return TRUE; + } + foreach (self::$trustedHostPatterns as $pattern) { + if (preg_match($pattern, $host)) { + self::$trustedHosts[] = $host; + return TRUE; + } + } + return FALSE; + } + // In cases where trusted_host_patterns are not set, allow all. This is + // flagged as a security issue by Drupal core in the Reports UI. + return TRUE; + } + +} diff --git a/domain_config/src/DomainConfigServiceProvider.php b/domain/src/DomainServiceProvider.php similarity index 73% rename from domain_config/src/DomainConfigServiceProvider.php rename to domain/src/DomainServiceProvider.php index a7841188..91ecb8b9 100644 --- a/domain_config/src/DomainConfigServiceProvider.php +++ b/domain/src/DomainServiceProvider.php @@ -1,11 +1,10 @@ getDefinition('router.route_provider'); - $definition->setClass(DomainRouteProvider::class); - + // Add the site context to the render cache. if ($container->hasParameter('renderer.config')) { $renderer_config = $container->getParameter('renderer.config'); diff --git a/domain/src/DomainStorage.php b/domain/src/DomainStorage.php new file mode 100644 index 00000000..8f917496 --- /dev/null +++ b/domain/src/DomainStorage.php @@ -0,0 +1,210 @@ +typedConfig = $typed_config; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('config.factory'), + $container->get('uuid'), + $container->get('language_manager'), + $container->get('entity.memory_cache'), + $container->get('config.typed') + ); + } + + /** + * {@inheritdoc} + */ + public function loadSchema() { + $fields = $this->typedConfig->getDefinition('domain.record.*'); + return isset($fields['mapping']) ? $fields['mapping'] : []; + } + + /** + * {@inheritdoc} + */ + public function loadDefaultId() { + $result = $this->loadDefaultDomain(); + if (!empty($result)) { + return $result->id(); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function loadDefaultDomain() { + $result = $this->loadByProperties(['is_default' => TRUE]); + if (!empty($result)) { + return current($result); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function loadMultipleSorted(array $ids = NULL) { + $domains = $this->loadMultiple($ids); + uasort($domains, [$this, 'sort']); + return $domains; + } + + /** + * {@inheritdoc} + */ + public function loadByHostname($hostname) { + $hostname = $this->prepareHostname($hostname); + $result = $this->loadByProperties(['hostname' => $hostname]); + if (empty($result)) { + return NULL; + } + return current($result); + } + + /** + * {@inheritdoc} + */ + public function loadOptionsList() { + $list = []; + foreach ($this->loadMultipleSorted() as $id => $domain) { + $list[$id] = $domain->label(); + } + return $list; + } + + /** + * {@inheritdoc} + */ + public function sort(DomainInterface $a, DomainInterface $b) { + // Prioritize the weights. + $weight_difference = $a->getWeight() - $b->getWeight(); + if ($weight_difference !== 0) { + return $weight_difference; + } + + // Fallback to the labels if the weights are equal. + return strcmp($a->label(), $b->label()); + } + + /** + * {@inheritdoc} + */ + public function prepareHostname($hostname) { + // Strip www. prefix off the hostname. + $ignore_www = $this->configFactory->get('domain.settings')->get('www_prefix'); + if ($ignore_www && substr($hostname, 0, 4) == 'www.') { + $hostname = substr($hostname, 4); + } + return $hostname; + } + + /** + * {@inheritdoc} + */ + public function create(array $values = []) { + $default = $this->loadDefaultId(); + $domains = $this->loadMultiple(); + if (empty($values)) { + $values['hostname'] = $this->createHostname(); + $values['name'] = \Drupal::config('system.site')->get('name'); + } + $values += [ + 'scheme' => $this->getDefaultScheme(), + 'status' => 1, + 'weight' => count($domains) + 1, + 'is_default' => (int) empty($default), + ]; + $domain = parent::create($values); + + return $domain; + } + + /** + * {@inheritdoc} + */ + public function createHostname() { + // We cannot inject the negotiator due to dependencies. + return \Drupal::service('domain.negotiator')->negotiateActiveHostname(); + } + + /** + * {@inheritdoc} + */ + public function createMachineName($hostname = NULL) { + if (empty($hostname)) { + $hostname = $this->createHostname(); + } + return preg_replace('/[^a-z0-9_]/', '_', $hostname); + } + + /** + * {@inheritdoc} + */ + public function getDefaultScheme() { + // Use the foundation request if possible. + $request = \Drupal::request(); + if (!empty($request)) { + $scheme = $request->getScheme(); + } + // Else use the server variable. + elseif (!empty($_SERVER['https'])) { + $scheme = 'https'; + } + // Else fall through to default. + else { + $scheme = 'http'; + } + return $scheme; + } + +} diff --git a/domain/src/DomainLoaderInterface.php b/domain/src/DomainStorageInterface.php similarity index 50% rename from domain/src/DomainLoaderInterface.php rename to domain/src/DomainStorageInterface.php index b250e0b2..115a6c22 100644 --- a/domain/src/DomainLoaderInterface.php +++ b/domain/src/DomainStorageInterface.php @@ -1,70 +1,40 @@ loader = $loader; + public function __construct(EntityTypeManagerInterface $entity_type_manager, DomainNegotiatorInterface $negotiator) { + $this->entityTypeManager = $entity_type_manager; + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); $this->negotiator = $negotiator; } @@ -51,64 +56,64 @@ public function __construct(DomainLoaderInterface $loader, DomainNegotiatorInter */ public function getTokenInfo() { // Domain token types. - $info['types']['domain'] = array( + $info['types']['domain'] = [ 'name' => $this->t('Domains'), 'description' => $this->t('Tokens related to domains.'), 'needs-data' => 'domain', - ); + ]; // These two types require the Token contrib module. - $info['types']['current-domain'] = array( + $info['types']['current-domain'] = [ 'name' => $this->t('Current domain'), 'description' => $this->t('Tokens related to the current domain.'), 'type' => 'domain', - ); - $info['types']['default-domain'] = array( + ]; + $info['types']['default-domain'] = [ 'name' => $this->t('Default domain'), 'description' => $this->t('Tokens related to the default domain.'), 'type' => 'domain', - ); + ]; // Domain tokens. - $info['tokens']['domain']['id'] = array( + $info['tokens']['domain']['id'] = [ 'name' => $this->t('Domain id'), - 'description' => $this->t('The domain\'s numeric ID.'), - ); - $info['tokens']['domain']['machine-name'] = array( + 'description' => $this->t("The domain's numeric ID."), + ]; + $info['tokens']['domain']['machine-name'] = [ 'name' => $this->t('Domain machine name'), 'description' => $this->t('The domain machine identifier.'), - ); - $info['tokens']['domain']['path'] = array( + ]; + $info['tokens']['domain']['path'] = [ 'name' => $this->t('Domain path'), 'description' => $this->t('The base URL for the domain.'), - ); - $info['tokens']['domain']['name'] = array( + ]; + $info['tokens']['domain']['name'] = [ 'name' => $this->t('Domain name'), 'description' => $this->t('The domain name.'), - ); - $info['tokens']['domain']['url'] = array( + ]; + $info['tokens']['domain']['url'] = [ 'name' => $this->t('Domain URL'), - 'description' => $this->t('The domain\'s URL for the current page request.'), - ); - $info['tokens']['domain']['hostname'] = array( + 'description' => $this->t("The domain's URL for the current page request."), + ]; + $info['tokens']['domain']['hostname'] = [ 'name' => $this->t('Domain hostname'), 'description' => $this->t('The domain hostname.'), - ); - $info['tokens']['domain']['scheme'] = array( + ]; + $info['tokens']['domain']['scheme'] = [ 'name' => $this->t('Domain scheme'), 'description' => $this->t('The domain scheme.'), - ); - $info['tokens']['domain']['status'] = array( + ]; + $info['tokens']['domain']['status'] = [ 'name' => $this->t('Domain status'), 'description' => $this->t('The domain status.'), - ); - $info['tokens']['domain']['weight'] = array( + ]; + $info['tokens']['domain']['weight'] = [ 'name' => $this->t('Domain weight'), 'description' => $this->t('The domain weight.'), - ); - $info['tokens']['domain']['is_default'] = array( + ]; + $info['tokens']['domain']['is_default'] = [ 'name' => $this->t('Domain default'), 'description' => $this->t('The domain is the default domain.'), - ); + ]; return $info; } @@ -117,7 +122,7 @@ public function getTokenInfo() { * Implements hook_tokens(). */ public function getTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { - $replacements = array(); + $replacements = []; $domain = NULL; @@ -131,11 +136,13 @@ public function getTokens($type, $tokens, array $data, array $options, Bubbleabl $domain = $this->negotiator->getActiveDomain(); } break; + case 'current-domain': $domain = $this->negotiator->getActiveDomain(); break; + case 'default-domain': - $domain = $this->loader->loadDefaultDomain(); + $domain = $this->domainStorage->loadDefaultDomain(); break; } @@ -159,6 +166,7 @@ public function getTokens($type, $tokens, array $data, array $options, Bubbleabl * We assume that the token will call an instance of DomainInterface. * * @return array + * An array of callbacks keyed by the token string. */ public function getCallbacks() { return [ diff --git a/domain/src/DomainValidator.php b/domain/src/DomainValidator.php index a833f16d..5d68100a 100644 --- a/domain/src/DomainValidator.php +++ b/domain/src/DomainValidator.php @@ -3,9 +3,9 @@ namespace Drupal\domain; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Component\Utility\Unicode; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; @@ -38,7 +38,14 @@ class DomainValidator implements DomainValidatorInterface { protected $configFactory; /** - * Constructs a DomainNegotiator object. + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a DomainValidator object. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. @@ -46,23 +53,21 @@ class DomainValidator implements DomainValidatorInterface { * The config factory. * @param \GuzzleHttp\ClientInterface $http_client * A Guzzle client object. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, ClientInterface $http_client) { + public function __construct(ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, ClientInterface $http_client, EntityTypeManagerInterface $entity_type_manager) { $this->moduleHandler = $module_handler; $this->configFactory = $config_factory; - // @TODO: Move to a proper service? $this->httpClient = $http_client; + $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} - * - * @TODO: Divide this into separate methods. Do not return Drupal-specific - * responses. */ - public function validate(DomainInterface $domain) { - $hostname = $domain->getHostname(); - $error_list = array(); + public function validate($hostname) { + $error_list = []; // Check for at least one dot or the use of 'localhost'. // Note that localhost can specify a port. $localhost_check = explode(':', $hostname); @@ -90,7 +95,8 @@ public function validate(DomainInterface $domain) { $error_list[] = $this->t('The domain must not end with a dot (.)'); } // Check for valid characters, unless using non-ASCII domains. - $non_ascii = $this->configFactory->get('domain.settings')->get('allow_non_ascii'); + $config = $this->configFactory->get('domain.settings'); + $non_ascii = $config->get('allow_non_ascii'); if (!$non_ascii) { $pattern = '/^[a-z0-9\.\-:]*$/i'; if (!preg_match($pattern, $hostname)) { @@ -98,41 +104,21 @@ public function validate(DomainInterface $domain) { } } // Check for lower case. - if ($hostname != Unicode::strtolower($hostname)) { + if ($hostname != mb_strtolower($hostname)) { $error_list[] = $this->t('Only lower-case characters are allowed.'); } // Check for 'www' prefix if redirection / handling is // enabled under global domain settings. // Note that www prefix handling must be set explicitly in the UI. // See http://drupal.org/node/1529316 and http://drupal.org/node/1783042 - if ($this->configFactory->get('domain.settings')->get('www_prefix') && (substr($hostname, 0, strpos($hostname, '.')) == 'www')) { + if ($config->get('www_prefix') && (substr($hostname, 0, strpos($hostname, '.')) == 'www')) { $error_list[] = $this->t('WWW prefix handling: Domains must be registered without the www. prefix.'); } - // Check existing domains. - $domains = \Drupal::entityTypeManager() - ->getStorage('domain') - ->loadByProperties(array('hostname' => $hostname)); - foreach ($domains as $match) { - if ($match->id() != $domain->id()) { - $error_list[] = $this->t('The hostname is already registered.'); - } - } // Allow modules to alter this behavior. - \Drupal::moduleHandler()->invokeAll('domain_validate', array($error_list, $hostname)); - - // Return the errors, if any. - if (!empty($error_list)) { - return $this->t('The domain string is invalid for %subdomain: @errors', array( - '%subdomain' => $hostname, - '@errors' => array( - '#theme' => 'item_list', - '#items' => $error_list, - ), - )); - } + $this->moduleHandler->alter('domain_validate', $error_list, $hostname); - return array(); + return $error_list; } /** @@ -146,20 +132,21 @@ public function checkResponse(DomainInterface $domain) { } // We cannot know which Guzzle Exception class will be returned; be generic. catch (RequestException $e) { - watchdog_exception('domain', $e); // File a general server failure. $domain->setResponse(500); - return; + return $domain->getResponse(); } // Expected result (i.e. no exception thrown.) $domain->setResponse($request->getStatusCode()); + + return $domain->getResponse(); } /** * {@inheritdoc} */ public function getRequiredFields() { - return array('hostname', 'name', 'id', 'scheme', 'status', 'weight'); + return ['hostname', 'name', 'scheme', 'status', 'weight']; } } diff --git a/domain/src/DomainValidatorInterface.php b/domain/src/DomainValidatorInterface.php index 58423ce8..b1c349c2 100644 --- a/domain/src/DomainValidatorInterface.php +++ b/domain/src/DomainValidatorInterface.php @@ -10,13 +10,13 @@ interface DomainValidatorInterface { /** * Validates the hostname for a domain. * - * @param \Drupal\domain\DomainInterface $domain - * A domain record. + * @param string $hostname + * A hostname to validate. * * @return array * An array of validation errors. An empty array indicates a valid domain. */ - public function validate(DomainInterface $domain); + public function validate($hostname); /** * Tests that a domain responds correctly. diff --git a/domain/src/DomainViewBuilder.php b/domain/src/DomainViewBuilder.php deleted file mode 100644 index fcd3f2e3..00000000 --- a/domain/src/DomainViewBuilder.php +++ /dev/null @@ -1,43 +0,0 @@ -bundle()]; - foreach ($list as $key) { - if (!empty($entity->{$key}) && $display->getComponent($key)) { - $class = str_replace('_', '-', $key); - $entity->content[$key] = array( - '#markup' => Html::escape($entity->{$key}), - '#prefix' => '
' . '' . Html::escape($key) . ':
', - '#suffix' => '
', - ); - } - } - } - } - -} diff --git a/domain/src/Entity/Domain.php b/domain/src/Entity/Domain.php index 199c815f..32f4b3e3 100644 --- a/domain/src/Entity/Domain.php +++ b/domain/src/Entity/Domain.php @@ -2,12 +2,14 @@ namespace Drupal\domain\Entity; +use Drupal\Core\Config\ConfigValueException; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\domain\DomainInterface; +use Drupal\domain\DomainNegotiatorInterface; /** * Defines the domain entity. @@ -17,11 +19,9 @@ * label = @Translation("Domain record"), * module = "domain", * handlers = { - * "storage" = "Drupal\Core\Config\Entity\ConfigEntityStorage", - * "view_builder" = "Drupal\domain\DomainViewBuilder", + * "storage" = "Drupal\domain\DomainStorage", * "access" = "Drupal\domain\DomainAccessControlHandler", * "list_builder" = "Drupal\domain\DomainListBuilder", - * "view_builder" = "Drupal\domain\DomainViewBuilder", * "form" = { * "default" = "Drupal\domain\DomainForm", * "edit" = "Drupal\domain\DomainForm", @@ -35,6 +35,7 @@ * "domain_id" = "domain_id", * "label" = "name", * "uuid" = "uuid", + * "status" = "status", * "weight" = "weight" * }, * links = { @@ -69,17 +70,10 @@ class Domain extends ConfigEntityBase implements DomainInterface { /** * The domain record ID. * - * @var integer + * @var int */ protected $domain_id; - /** - * The domain record UUID. - * - * @var string - */ - protected $uuid; - /** * The domain list name (e.g. Drupal). * @@ -94,26 +88,19 @@ class Domain extends ConfigEntityBase implements DomainInterface { */ protected $hostname; - /** - * The domain status. - * - * @var boolean - */ - protected $status; - /** * The domain record sort order. * - * @var integer + * @var int */ protected $weight; /** * Indicates the default domain. * - * @var boolean + * @var bool */ - protected $is_default; + protected $is_default = FALSE; /** * The domain record protocol (e.g. http://). @@ -139,40 +126,45 @@ class Domain extends ConfigEntityBase implements DomainInterface { /** * The domain record http response test (e.g. 200), a calculated value. * - * @var integer + * @var int */ protected $response = NULL; /** * The redirect method to use, if needed. + * + * @var int|null */ protected $redirect = NULL; /** * The type of match returned by the negotiator. + * + * @var int */ protected $matchType; /** - * Overrides Drupal\Core\Entity\Entity:preCreate(). + * The canonical hostname for the domain. * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage_controller - * The entity storage object. - * @param mixed[] $values - * An array of values to set, keyed by property name. If the entity type has - * bundles the bundle key has to be specified. + * @var string + */ + protected $canonical; + + /** + * {@inheritdoc} */ public static function preCreate(EntityStorageInterface $storage_controller, array &$values) { parent::preCreate($storage_controller, $values); - $loader = \Drupal::service('domain.loader'); - $default = $loader->loadDefaultId(); - $domains = $loader->loadMultiple(); - $values += array( + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $default = $domain_storage->loadDefaultId(); + $count = $storage_controller->getQuery()->count()->execute(); + $values += [ 'scheme' => empty($GLOBALS['is_https']) ? 'http' : 'https', 'status' => 1, - 'weight' => count($domains) + 1, + 'weight' => $count + 1, 'is_default' => (int) empty($default), - ); + ]; // Note that we have not created a domain_id, which is only used for // node access control and will be added on save. } @@ -182,7 +174,7 @@ public static function preCreate(EntityStorageInterface $storage_controller, arr */ public function isActive() { $negotiator = \Drupal::service('domain.negotiator'); - /** @var DomainInterface $domain */ + /** @var self $domain */ $domain = $negotiator->getActiveDomain(); if (empty($domain)) { return FALSE; @@ -219,38 +211,42 @@ public function isHttps() { public function saveDefault() { if (!$this->isDefault()) { // Swap the current default. - /** @var DomainInterface $default */ - if ($default = \Drupal::service('domain.loader')->loadDefaultDomain()) { - $default->is_default = 0; + /** @var self $default */ + if ($default = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultDomain()) { + $default->is_default = FALSE; + $default->setHostname($default->getCanonical()); $default->save(); } // Save the new default. - $this->is_default = 1; + $this->is_default = TRUE; + $this->setHostname($this->getCanonical()); $this->save(); } else { - drupal_set_message($this->t('The selected domain is already the default.'), 'warning'); + \Drupal::messenger()->addMessage($this->t('The selected domain is already the default.'), 'warning'); } } /** - * Overrides Drupal\Core\Config\Entity\ConfigEntityBase::enable(). + * {@inheritdoc} */ public function enable() { $this->setStatus(TRUE); + $this->setHostname($this->getCanonical()); $this->save(); } /** - * Overrides Drupal\Core\Config\Entity\ConfigEntityBase::disable(). + * {@inheritdoc} */ public function disable() { if (!$this->isDefault()) { $this->setStatus(FALSE); + $this->setHostname($this->getCanonical()); $this->save(); } else { - drupal_set_message($this->t('The default domain cannot be disabled.'), 'warning'); + \Drupal::messenger()->addMessage($this->t('The default domain cannot be disabled.'), 'warning'); } } @@ -260,15 +256,16 @@ public function disable() { public function saveProperty($name, $value) { if (isset($this->{$name})) { $this->{$name} = $value; + $this->setHostname($this->getCanonical()); $this->save(); - drupal_set_message($this->t('The @key attribute was set to @value for domain @hostname.', array( + \Drupal::messenger()->addMessage($this->t('The @key attribute was set to @value for domain @hostname.', [ '@key' => $name, '@value' => $value, '@hostname' => $this->hostname, - ))); + ])); } else { - drupal_set_message($this->t('The @key attribute does not exist.', array('@key' => $name))); + \Drupal::messenger()->addMessage($this->t('The @key attribute does not exist.', ['@key' => $name])); } } @@ -276,14 +273,16 @@ public function saveProperty($name, $value) { * {@inheritdoc} */ public function setPath() { - $this->path = $this->getScheme() . $this->getHostname() . base_path(); + global $base_path; + $this->path = $this->getScheme() . $this->getHostname() . ($base_path ?: '/'); } /** * {@inheritdoc} */ public function setUrl() { - $uri = \Drupal::request()->getRequestUri(); + $request = \Drupal::request(); + $uri = $request ? $request->getRequestUri() : '/'; $this->url = $this->getScheme() . $this->getHostname() . $uri; } @@ -329,28 +328,33 @@ public function getUrl() { } /** - * Overrides Drupal\Core\Config\Entity\ConfigEntityBase::preSave(). - * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage_controller - * The entity storage object. + * {@inheritdoc} */ - public function preSave(EntityStorageInterface $storage_controller) { + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); // Sets the default domain properly. - $loader = \Drupal::service('domain.loader'); - /** @var DomainInterface $default */ - $default = $loader->loadDefaultDomain(); + /** @var self $default */ + $default = $storage->loadDefaultDomain(); if (!$default) { - $this->is_default = 1; + $this->is_default = TRUE; } - elseif ($this->is_default && $default->id() != $this->id()) { + elseif ($this->is_default && $default->getDomainId() != $this->getDomainId()) { // Swap the current default. - $default->is_default = 0; + $default->is_default = FALSE; $default->save(); } - // Ensures we have a proper domain_id. - if ($this->isNew()) { + // Ensures we have a proper domain_id but does not erase existing ones. + if ($this->isNew() && empty($this->getDomainId())) { $this->createDomainId(); } + // Prevent duplicate hostname. + $hostname = $this->getHostname(); + // Do not use domain loader because it may change hostname. + $existing = $storage->loadByProperties(['hostname' => $hostname]); + $existing = reset($existing); + if ($existing && $this->getDomainId() != $existing->getDomainId()) { + throw new ConfigValueException("The hostname ($hostname) is already registered."); + } } /** @@ -362,6 +366,26 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { \Drupal::service('cache_tags.invalidator')->invalidateTags(['rendered', 'url.site']); } + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + foreach ($entities as $entity) { + $actions = $storage->loadMultiple([ + 'domain_default_action.' . $entity->id(), + 'domain_delete_action.' . $entity->id(), + 'domain_disable_action.' . $entity->id(), + 'domain_enable_action.' . $entity->id(), + ]); + foreach ($actions as $action) { + $action->delete(); + } + } + // Invalidate cache tags relevant to domains. + \Drupal::service('cache_tags.invalidator')->invalidateTags(['rendered', 'url.site']); + } + /** * {@inheritdoc} */ @@ -370,7 +394,32 @@ public function createDomainId() { // across environments. Instead, we use the crc32 hash function to create a // unique numeric id for each domain. In some systems (Windows?) we have // reports of crc32 returning a negative number. Issue #2794047. - $this->domain_id = abs((int) crc32($this->id())); + // If we don't use hash(), then crc32() returns different results for 32- + // and 64-bit systems. On 32-bit systems, the number returned may also be + // too large for PHP. + // See #2908236. + $id = hash('crc32', $this->id()); + $id = abs(hexdec(substr($id, 0, -2))); + $this->createNumericId($id); + } + + /** + * Creates a unique numeric id for use in the {node_access} table. + * + * @param int $id + * An integer to use as the numeric id. + */ + public function createNumericId($id) { + // Ensure that this value is unique. + $storage = \Drupal::entityTypeManager()->getStorage('domain'); + $result = $storage->loadByProperties(['domain_id' => $id]); + if (empty($result)) { + $this->domain_id = $id; + } + else { + $id++; + $this->createNumericId($id); + } } /** @@ -378,7 +427,10 @@ public function createDomainId() { */ public function getScheme($add_suffix = TRUE) { $scheme = $this->scheme; - if ($scheme != 'https') { + if ($scheme == 'variable') { + $scheme = \Drupal::entityTypeManager()->getStorage('domain')->getDefaultScheme(); + } + elseif ($scheme != 'https') { $scheme = 'http'; } $scheme .= ($add_suffix) ? '://' : ''; @@ -386,6 +438,13 @@ public function getScheme($add_suffix = TRUE) { return $scheme; } + /** + * {@inheritdoc} + */ + public function getRawScheme() { + return $this->scheme; + } + /** * {@inheritdoc} */ @@ -408,7 +467,7 @@ public function setResponse($response) { * {@inheritdoc} */ public function getLink($current_path = TRUE) { - $options = array('absolute' => TRUE, 'https' => $this->isHttps()); + $options = ['absolute' => TRUE, 'https' => $this->isHttps()]; if ($current_path) { $url = Url::fromUri($this->getUrl(), $options); } @@ -416,7 +475,7 @@ public function getLink($current_path = TRUE) { $url = Url::fromUri($this->getPath(), $options); } - return Link::fromTextAndUrl($this->getHostname(), $url)->toString(); + return Link::fromTextAndUrl($this->getCanonical(), $url)->toString(); } /** @@ -464,7 +523,7 @@ public function getWeight() { /** * {@inheritdoc} */ - public function setMatchType($match_type = \Drupal\domain\DomainNegotiator::DOMAIN_MATCH_EXACT) { + public function setMatchType($match_type = DomainNegotiatorInterface::DOMAIN_MATCHED_EXACT) { $this->matchType = $match_type; } @@ -475,11 +534,49 @@ public function getMatchType() { return $this->matchType; } - /** - * This is being fired for some reason if the values are shown on a - * content type. - */ - public function isDefaultRevision($new_value = NULL) { - return TRUE; + /** + * {@inheritdoc} + */ + public function getPort() { + $ports = explode(':', $this->getHostname()); + if (isset($ports[1])) { + return ':' . $ports[1]; + } + return ''; + } + + /** + * {@inheritdoc} + */ + public function setCanonical($hostname = NULL) { + if (is_null($hostname)) { + $this->canonical = $this->getHostname(); + } + else { + $this->canonical = $hostname; + } + } + + /** + * {@inheritdoc} + */ + public function getCanonical() { + if (empty($this->canonical)) { + $this->setCanonical(); + } + return $this->canonical; } + + /** + * Prevent render errors when Twig wants to read this object. + * + * @see \Drupal\Core\Template\TwigExtension::escapeFilter() + * + * @return string + * The name of the domain being rendered. + */ + public function toString() { + return $this->name; + } + } diff --git a/domain/src/EventSubscriber/DomainSubscriber.php b/domain/src/EventSubscriber/DomainSubscriber.php index 21f5a244..a170d3bc 100644 --- a/domain/src/EventSubscriber/DomainSubscriber.php +++ b/domain/src/EventSubscriber/DomainSubscriber.php @@ -4,12 +4,14 @@ use Drupal\domain\Access\DomainAccessCheck; use Drupal\domain\DomainNegotiatorInterface; -use Drupal\domain\DomainLoaderInterface; +use Drupal\domain\DomainRedirectResponse; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\Session\AccountInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpFoundation\Response; /** * Sets the domain context for an http request. @@ -24,11 +26,18 @@ class DomainSubscriber implements EventSubscriberInterface { protected $domainNegotiator; /** - * The domain loader service. + * The entity type manager. * - * @var \Drupal\domain\DomainLoaderInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $domainLoader; + protected $entityTypeManager; + + /** + * The Domain storage handler service. + * + * @var \Drupal\domain\DomainStorageInterface + */ + protected $domainStorage; /** * The core access check service. @@ -49,16 +58,17 @@ class DomainSubscriber implements EventSubscriberInterface { * * @param \Drupal\domain\DomainNegotiatorInterface $negotiator * The domain negotiator service. - * @param \Drupal\domain\DomainLoaderInterface $loader - * The domain loader. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. * @param \Drupal\domain\Access\DomainAccessCheck $access_check * The access check interface. * @param \Drupal\Core\Session\AccountInterface $account * The current user account. */ - public function __construct(DomainNegotiatorInterface $negotiator, DomainLoaderInterface $loader, DomainAccessCheck $access_check, AccountInterface $account) { + public function __construct(DomainNegotiatorInterface $negotiator, EntityTypeManagerInterface $entity_type_manager, DomainAccessCheck $access_check, AccountInterface $account) { $this->domainNegotiator = $negotiator; - $this->domainLoader = $loader; + $this->entityTypeManager = $entity_type_manager; + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); $this->accessCheck = $access_check; $this->account = $account; } @@ -81,13 +91,14 @@ public function onKernelRequestDomain(GetResponseEvent $event) { // Negotiate the request and set domain context. /** @var \Drupal\domain\DomainInterface $domain */ if ($domain = $this->domainNegotiator->getActiveDomain(TRUE)) { + $hostname = $domain->getHostname(); $domain_url = $domain->getUrl(); if ($domain_url) { $redirect_status = $domain->getRedirect(); $path = trim($event->getRequest()->getPathInfo(), '/'); // If domain negotiation asked for a redirect, issue it. if (is_null($redirect_status) && $this->accessCheck->checkPath($path)) { - // Else check for active domain or inactive access. + // Else check for active domain or inactive access. /** @var \Drupal\Core\Access\AccessResult $access */ $access = $this->accessCheck->access($this->account); // If the access check fails, reroute to the default domain. @@ -95,15 +106,23 @@ public function onKernelRequestDomain(GetResponseEvent $event) { // We insist on Allowed. if (!$access->isAllowed()) { /** @var \Drupal\domain\DomainInterface $default */ - $default = $this->domainLoader->loadDefaultDomain(); + $default = $this->domainStorage->loadDefaultDomain(); $domain_url = $default->getUrl(); $redirect_status = 302; + $hostname = $default->getHostname(); } } } - if (isset($redirect_status)) { + if (!empty($redirect_status)) { // Pass a redirect if necessary. - $response = new TrustedRedirectResponse($domain_url, $redirect_status); + if (DomainRedirectResponse::checkTrustedHost($hostname)) { + $response = new TrustedRedirectResponse($domain_url, $redirect_status); + } + else { + // If the redirect is not to a registered hostname, reject the + // request. + $response = new Response('The provided host name is not a valid redirect.', 401); + } $event->setResponse($response); } } @@ -114,7 +133,7 @@ public function onKernelRequestDomain(GetResponseEvent $event) { */ public static function getSubscribedEvents() { // This needs to fire very early in the stack, before accounts are cached. - $events[KernelEvents::REQUEST][] = array('onKernelRequestDomain', 50); + $events[KernelEvents::REQUEST][] = ['onKernelRequestDomain', 50]; return $events; } diff --git a/domain/src/Form/DomainDeleteForm.php b/domain/src/Form/DomainDeleteForm.php index d556a7c4..58914861 100644 --- a/domain/src/Form/DomainDeleteForm.php +++ b/domain/src/Form/DomainDeleteForm.php @@ -15,7 +15,7 @@ class DomainDeleteForm extends EntityConfirmFormBase { * {@inheritdoc} */ public function getQuestion() { - return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label())); + return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); } /** @@ -37,8 +37,8 @@ public function getConfirmText() { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); - drupal_set_message($this->t('Domain %label has been deleted.', array('%label' => $this->entity->label()))); - \Drupal::logger('domain')->notice('Domain %label has been deleted.', array('%label' => $this->entity->label())); + \Drupal::messenger()->addMessage($this->t('Domain %label has been deleted.', ['%label' => $this->entity->label()])); + \Drupal::logger('domain')->notice('Domain %label has been deleted.', ['%label' => $this->entity->label()]); $form_state->setRedirectUrl($this->getCancelUrl()); } diff --git a/domain/src/Form/DomainSettingsForm.php b/domain/src/Form/DomainSettingsForm.php index bbc63a63..bc25dabc 100644 --- a/domain/src/Form/DomainSettingsForm.php +++ b/domain/src/Form/DomainSettingsForm.php @@ -1,15 +1,17 @@ config('domain.settings'); - $form['allow_non_ascii'] = array( + $form['allow_non_ascii'] = [ '#type' => 'checkbox', - '#title' => $this->t('Allow non-ASCII characters in domains and aliases.'), + '#title' => $this->t('Allow non-ASCII characters in domains and aliases'), '#default_value' => $config->get('allow_non_ascii'), '#description' => $this->t('Domains may be registered with international character sets. Note that not all DNS server respect non-ascii characters.'), - ); - $form['www_prefix'] = array( + ]; + $form['www_prefix'] = [ '#type' => 'checkbox', '#title' => $this->t('Ignore www prefix when negotiating domains'), '#default_value' => $config->get('www_prefix'), '#description' => $this->t('Domain negotiation will ignore any www prefixes for all requests.'), - ); + ]; // Get the usable tokens for this field. + $patterns = []; foreach (\Drupal::service('domain.token')->getCallbacks() as $key => $callback) { $patterns[] = "[domain:$key]"; } - $form['css_classes'] = array( + $form['css_classes'] = [ '#type' => 'textfield', '#size' => 80, '#title' => $this->t('Custom CSS classes'), '#default_value' => $config->get('css_classes'), - '#description' => $this->t('Enter any CSS classes that should be added to the <body> tag. Available replacement patterns are: ' . implode(', ', $patterns)), - ); - $form['login_paths'] = array( + '#description' => $this->t('Enter any CSS classes that should be added to the <body> tag. Available replacement patterns are: @patterns', [ + '@patterns' => implode(', ', $patterns), + ]), + ]; + $form['login_paths'] = [ '#type' => 'textarea', '#rows' => 5, '#columns' => 40, '#title' => $this->t('Paths that should be accessible for inactive domains'), - '#default_value' => $config->get('login_paths', "/user/login\r\n/user/password"), + '#default_value' => $config->get('login_paths'), '#description' => $this->t('Inactive domains are only accessible to users with permission. Enter any paths that should be accessible, one per line. Normally, only the login path will be allowed.'), - ); + ]; return parent::buildForm($form, $form_state); } diff --git a/domain/src/Plugin/Block/DomainNavBlock.php b/domain/src/Plugin/Block/DomainNavBlock.php new file mode 100644 index 00000000..fb9c8a8c --- /dev/null +++ b/domain/src/Plugin/Block/DomainNavBlock.php @@ -0,0 +1,216 @@ +getSetting('link_options') == 'home') { + return ['url.site']; + } + return ['url']; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $defaults = $this->defaultConfiguration(); + $elements['link_options'] = [ + '#title' => $this->t('Link paths'), + '#type' => 'radios', + '#required' => TRUE, + '#options' => ['active' => $this->t('Link to active url'), 'home' => $this->t('Link to site home page')], + '#default_value' => !empty($this->configuration['link_options']) ? $this->configuration['link_options'] : $defaults['link_options'], + '#description' => $this->t('Determines how links to each domain will be written. Note that some paths may not be accessible on all domains.'), + ]; + $options = [ + 'select' => $this->t('JavaScript select list'), + 'menus' => $this->t('Menu-style tab links'), + 'ul' => $this->t('Unordered list of links'), + ]; + $elements['link_theme'] = [ + '#type' => 'radios', + '#title' => t('Link theme'), + '#default_value' => !empty($this->configuration['link_theme']) ? $this->configuration['link_theme'] : $defaults['link_theme'], + '#options' => $options, + '#description' => $this->t('Select how to display the block output.'), + ]; + $options = [ + 'name' => $this->t('The domain display name'), + 'hostname' => $this->t('The raw hostname'), + 'url' => $this->t('The domain base URL'), + ]; + $elements['link_label'] = [ + '#type' => 'radios', + '#title' => $this->t('Link text'), + '#default_value' => !empty($this->configuration['link_label']) ? $this->configuration['link_label'] : $defaults['link_label'], + '#options' => $options, + '#description' => $this->t('Select the text to display for each link.'), + ]; + return $elements; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + // Process the block's submission handling if no errors occurred only. + if (!$form_state->getErrors()) { + foreach (array_keys($this->defaultConfiguration()) as $element) { + $this->configuration[$element] = $form_state->getValue($element); + } + } + } + + /** + * Overrides \Drupal\block\BlockBase::access(). + */ + public function access(AccountInterface $account, $return_as_object = FALSE) { + $access = AccessResult::allowedIfHasPermissions($account, ['administer domains', 'use domain nav block'], 'OR'); + return $return_as_object ? $access : $access->isAllowed(); + } + + /** + * Build the output. + */ + public function build() { + /** @var \Drupal\domain\DomainInterface $active_domain */ + $active_domain = \Drupal::service('domain.negotiator')->getActiveDomain(); + $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('domain'); + $account = \Drupal::currentUser(); + + // Determine the visible domain list. + $items = []; + $add_path = ($this->getSetting('link_options') == 'active'); + /** @var \Drupal\domain\DomainInterface $domain */ + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultipleSorted() as $domain) { + // Set the URL. + if ($add_path) { + $url = Url::fromUri($domain->getUrl()); + } + else { + $url = Url::fromUri($domain->getPath()); + } + // Set the label text. + $label = $domain->label(); + if ($this->getSetting('link_label') == 'hostname') { + $label = $domain->getHostname(); + } + elseif ($this->getSetting('link_label') == 'url') { + $label = $domain->getPath(); + } + // Handles menu links. + if ($domain->status() || $account->hasPermission('access inactive domains')) { + if ($this->getSetting('link_theme') == 'menus') { + // @TODO: active trail isn't working properly, likely because this + // isn't really a menu. + $items[] = [ + 'title' => $label, + 'url' => $url, + 'attributes' => [], + 'below' => [], + 'is_expanded' => FALSE, + 'is_collapsed' => FALSE, + 'in_active_trail' => $domain->isActive(), + ]; + } + elseif ($this->getSetting('link_theme') == 'select') { + $items[] = [ + 'label' => $label, + 'url' => $url->toString(), + 'active' => $domain->isActive(), + ]; + } + else { + $items[] = ['#markup' => Link::fromTextAndUrl($label, $url)->toString()]; + } + } + } + + // Set the proper theme. + switch ($this->getSetting('link_theme')) { + case 'select': + $build['#theme'] = 'domain_nav_block'; + $build['#items'] = $items; + break; + + case 'menus': + // Map the $items params to what menu.html.twig expects. + $build['#items'] = $items; + $build['#menu_name'] = 'domain-nav'; + $build['#sorted'] = TRUE; + $build['#theme'] = 'menu__' . strtr($build['#menu_name'], '-', '_'); + break; + + case 'ul': + default: + $build['#theme'] = 'item_list'; + $build['#items'] = $items; + break; + } + + return $build; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'link_options' => 'home', + 'link_theme' => 'ul', + 'link_label' => 'name', + ]; + } + + /** + * Gets the configuration for the block, loading defaults if not set. + * + * @param string $key + * The setting key to retrieve, a string. + * + * @return string + * The setting value, a string. + */ + public function getSetting($key) { + if (isset($this->settings[$key])) { + return $this->settings[$key]; + } + $defaults = $this->defaultConfiguration(); + if (isset($this->configuration[$key])) { + $this->settings[$key] = $this->configuration[$key]; + } + else { + $this->settings[$key] = $defaults[$key]; + } + return $this->settings[$key]; + } + +} diff --git a/domain/src/Plugin/Block/DomainServerBlock.php b/domain/src/Plugin/Block/DomainServerBlock.php index b9a70581..a2b0bfc1 100644 --- a/domain/src/Plugin/Block/DomainServerBlock.php +++ b/domain/src/Plugin/Block/DomainServerBlock.php @@ -5,7 +5,6 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Access\AccessResult; use Drupal\Core\Session\AccountInterface; -use Drupal\domain\DomainInterface; /** * Provides a server information block for a domain request. @@ -21,50 +20,69 @@ class DomainServerBlock extends DomainBlockBase { * Overrides \Drupal\block\BlockBase::access(). */ public function access(AccountInterface $account, $return_as_object = FALSE) { - $access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'view domain information'), 'OR'); + $access = AccessResult::allowedIfHasPermissions($account, ['administer domains', 'view domain information'], 'OR'); return $return_as_object ? $access : $access->isAllowed(); } /** * Build the output. - * - * @TODO: abstract or theme this function? */ public function build() { /** @var \Drupal\domain\DomainInterface $domain */ $domain = \Drupal::service('domain.negotiator')->getActiveDomain(); if (!$domain) { - return array( + return [ '#markup' => $this->t('No domain record could be loaded.'), - ); + ]; } - $header = array($this->t('Server'), $this->t('Value')); - $rows[] = array( + $header = [$this->t('Server'), $this->t('Value')]; + $rows[] = [ $this->t('HTTP_HOST request'), Html::escape($_SERVER['HTTP_HOST']), - ); + ]; // Check the response test. $domain->getResponse(); - $check = \Drupal::service('domain.loader')->loadByHostname($_SERVER['HTTP_HOST']); + $check = \Drupal::entityTypeManager()->getStorage('domain')->loadByHostname($_SERVER['HTTP_HOST']); $match = $this->t('Exact match'); + // This value is not translatable. + $environment = 'default'; if (!$check) { // Specific check for Domain Alias. if (isset($domain->alias)) { - $match = $this->t('ALIAS: Using alias %id', array('%id' => $domain->alias)); + $match = $this->t('ALIAS: Using alias %id', ['%id' => $domain->alias->getPattern()]); + // Get the environment. + $environment = $domain->alias->getEnvironment(); } else { $match = $this->t('FALSE: Using default domain.'); } } - $rows[] = array( + $rows[] = [ $this->t('Domain match'), $match, - ); + ]; + $rows[] = [ + $this->t('Environment'), + $environment, + ]; + $rows[] = [ + $this->t('Canonical hostname'), + $domain->getCanonical(), + ]; + $rows[] = [ + $this->t('Base path'), + $domain->getPath(), + ]; + $rows[] = [ + $this->t('Current URL'), + $domain->getUrl(), + ]; + $www = \Drupal::config('domain.settings')->get('www_prefix'); - $rows[] = array( + $rows[] = [ $this->t('Strip www prefix'), !empty($www) ? $this->t('On') : $this->t('Off'), - ); + ]; $list = $domain->toArray(); ksort($list); foreach ($list as $key => $value) { @@ -80,16 +98,16 @@ public function build() { elseif ($key == 'status' || $key == 'is_default') { $value = empty($value) ? $this->t('FALSE') : $this->t('TRUE'); } - $rows[] = array( + $rows[] = [ Html::escape($key), !is_array($value) ? Html::escape($value) : $this->printArray($value), - ); + ]; } - return array( + return [ '#theme' => 'table', '#rows' => $rows, '#header' => $header, - ); + ]; } /** @@ -102,24 +120,24 @@ public function build() { * A suitable output string. */ public function printArray(array $array) { - $items = array(); + $items = []; foreach ($array as $key => $val) { if (!is_array($val)) { $value = Html::escape($val); } else { - $list = array(); + $list = []; foreach ($val as $k => $v) { - $list[] = $this->t('@key : @value', array('@key' => $k, '@value' => $v)); + $list[] = $this->t('@key : @value', ['@key' => $k, '@value' => $v]); } $value = implode('
', $list); } - $items[] = $this->t('@key : @value', array('@key' => $key, '@value' => $value)); + $items[] = $this->t('@key : @value', ['@key' => $key, '@value' => $value]); } - $variables['domain_server'] = array( + $variables['domain_server'] = [ '#theme' => 'item_list', '#items' => $items, - ); + ]; return render($variables); } diff --git a/domain/src/Plugin/Block/DomainSwitcherBlock.php b/domain/src/Plugin/Block/DomainSwitcherBlock.php index e79343a8..25428f5a 100644 --- a/domain/src/Plugin/Block/DomainSwitcherBlock.php +++ b/domain/src/Plugin/Block/DomainSwitcherBlock.php @@ -10,7 +10,7 @@ * * @Block( * id = "domain_switcher_block", - * admin_label = @Translation("Domain switcher") + * admin_label = @Translation("Domain switcher (for admins and testing)") * ) */ class DomainSwitcherBlock extends DomainBlockBase { @@ -19,21 +19,19 @@ class DomainSwitcherBlock extends DomainBlockBase { * Overrides \Drupal\block\BlockBase::access(). */ public function access(AccountInterface $account, $return_as_object = FALSE) { - $access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'use domain switcher block'), 'OR'); + $access = AccessResult::allowedIfHasPermissions($account, ['administer domains', 'use domain switcher block'], 'OR'); return $return_as_object ? $access : $access->isAllowed(); } /** * Build the output. - * - * @TODO: abstract or theme this function? */ public function build() { /** @var \Drupal\domain\DomainInterface $active_domain */ $active_domain = \Drupal::service('domain.negotiator')->getActiveDomain(); - $items = array(); + $items = []; /** @var \Drupal\domain\DomainInterface $domain */ - foreach (\Drupal::service('domain.loader')->loadMultipleSorted() as $domain) { + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultipleSorted() as $domain) { $string = $domain->getLink(); if (!$domain->status()) { $string .= '*'; @@ -41,12 +39,12 @@ public function build() { if ($domain->id() == $active_domain->id()) { $string = '' . $string . ''; } - $items[] = array('#markup' => $string); + $items[] = ['#markup' => $string]; } - return array( + return [ '#theme' => 'item_list', '#items' => $items, - ); + ]; } } diff --git a/domain/src/Plugin/Block/DomainTokenBlock.php b/domain/src/Plugin/Block/DomainTokenBlock.php index f8f9965f..30fb54ef 100644 --- a/domain/src/Plugin/Block/DomainTokenBlock.php +++ b/domain/src/Plugin/Block/DomainTokenBlock.php @@ -2,7 +2,6 @@ namespace Drupal\domain\Plugin\Block; -use Drupal\Component\Utility\Html; use Drupal\Core\Access\AccessResult; use Drupal\Core\Session\AccountInterface; use Drupal\domain\DomainInterface; @@ -21,41 +20,40 @@ class DomainTokenBlock extends DomainBlockBase { * Overrides \Drupal\block\BlockBase::access(). */ public function access(AccountInterface $account, $return_as_object = FALSE) { - $access = AccessResult::allowedIfHasPermissions($account, array('administer domains', 'view domain information'), 'OR'); + $access = AccessResult::allowedIfHasPermissions($account, ['administer domains', 'view domain information'], 'OR'); return $return_as_object ? $access : $access->isAllowed(); } /** * Build the output. - * - * @TODO: abstract or theme this function? */ public function build() { /** @var \Drupal\domain\DomainInterface $domain */ $domain = \Drupal::service('domain.negotiator')->getActiveDomain(); if (!$domain) { - return array( + return [ '#markup' => $this->t('No domain record could be loaded.'), - ); + ]; } - $header = array($this->t('Token'), $this->t('Value')); - return array( + $header = [$this->t('Token'), $this->t('Value')]; + return [ '#theme' => 'table', '#rows' => $this->renderTokens($domain), '#header' => $header, - ); + ]; } /** * Generates available tokens for printing. * - * @param Drupal\domain\DomainInterface $domain + * @param \Drupal\domain\DomainInterface $domain * The active domain request. + * * @return array * An array keyed by token name, with value of replacement value. */ private function renderTokens(DomainInterface $domain) { - $rows = array(); + $rows = []; $token = \Drupal::token(); $tokens = $token->getInfo(); // The 'domain' token is supported by core. The others by Token module, diff --git a/domain/src/Plugin/Condition/Domain.php b/domain/src/Plugin/Condition/Domain.php index cb083d5f..9cda4bf1 100644 --- a/domain/src/Plugin/Condition/Domain.php +++ b/domain/src/Plugin/Condition/Domain.php @@ -14,8 +14,8 @@ * @Condition( * id = "domain", * label = @Translation("Domain"), - * context = { - * "entity:domain" = @ContextDefinition("entity:domain", label = @Translation("Domain"), required = FALSE) + * context_definitions = { + * "domain" = @ContextDefinition("entity:domain", label = @Translation("Domain"), required = TRUE) * } * ) */ @@ -61,18 +61,19 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['domains'] = array( + $form['domains'] = [ '#type' => 'checkboxes', '#title' => $this->t('When the following domains are active'), '#default_value' => $this->configuration['domains'], - '#options' => array_map('\Drupal\Component\Utility\Html::escape', \Drupal::service('domain.loader')->loadOptionsList()), + '#options' => array_map('\Drupal\Component\Utility\Html::escape', \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList()), '#description' => $this->t('If you select no domains, the condition will evaluate to TRUE for all requests.'), - '#attached' => array( - 'library' => array( + '#attached' => [ + 'library' => [ 'domain/drupal.domain', - ), - ), - ); + ], + ], + ]; + return parent::buildConfigurationForm($form, $form_state); } @@ -80,9 +81,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * {@inheritdoc} */ public function defaultConfiguration() { - return array( - 'domains' => array(), - ) + parent::defaultConfiguration(); + return [ + 'domains' => [], + ] + parent::defaultConfiguration(); } /** @@ -98,7 +99,7 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s */ public function summary() { // Use the domain labels. They will be sanitized below. - $domains = array_intersect_key(\Drupal::service('domain.loader')->loadOptionsList(), $this->configuration['domains']); + $domains = array_intersect_key(\Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(), $this->configuration['domains']); if (count($domains) > 1) { $domains = implode(', ', $domains); } @@ -106,10 +107,10 @@ public function summary() { $domains = reset($domains); } if ($this->isNegated()) { - return $this->t('Active domain is not @domains', array('@domains' => $domains)); + return $this->t('Active domain is not @domains', ['@domains' => $domains]); } else { - return $this->t('Active domain is @domains', array('@domains' => $domains)); + return $this->t('Active domain is @domains', ['@domains' => $domains]); } } @@ -122,7 +123,7 @@ public function evaluate() { return TRUE; } // If the context did not load, derive from the request. - if (!$domain = $this->getContextValue('entity:domain')) { + if (!$domain = $this->getContextValue('domain')) { $domain = $this->domainNegotiator->getActiveDomain(); } // No context found? diff --git a/domain/src/Plugin/EntityReferenceSelection/DomainAdminSelection.php b/domain/src/Plugin/EntityReferenceSelection/DomainAdminSelection.php index bd47b8a4..55666021 100644 --- a/domain/src/Plugin/EntityReferenceSelection/DomainAdminSelection.php +++ b/domain/src/Plugin/EntityReferenceSelection/DomainAdminSelection.php @@ -2,8 +2,6 @@ namespace Drupal\domain\Plugin\EntityReferenceSelection; -use Drupal\domain\Plugin\EntityReferenceSelection\DomainSelection; - /** * Provides entity reference selections for the domain entity type. * @@ -31,6 +29,6 @@ class DomainAdminSelection extends DomainSelection { * * @var string */ - protected $field_type = 'admin'; + protected $fieldType = 'admin'; } diff --git a/domain/src/Plugin/EntityReferenceSelection/DomainSelection.php b/domain/src/Plugin/EntityReferenceSelection/DomainSelection.php index 66b30723..208a887b 100644 --- a/domain/src/Plugin/EntityReferenceSelection/DomainSelection.php +++ b/domain/src/Plugin/EntityReferenceSelection/DomainSelection.php @@ -24,7 +24,7 @@ class DomainSelection extends DefaultSelection { * * @var string */ - protected $field_type = 'editor'; + protected $fieldType = 'editor'; /** * {@inheritdoc} @@ -47,7 +47,7 @@ public function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { if (!empty($info->configuration['entity'])) { $context['entity_type'] = $info->configuration['entity']->getEntityTypeId(); $context['bundle'] = $info->configuration['entity']->bundle(); - $context['field_type'] = $this->field_type; + $context['field_type'] = $this->fieldType; // Load the current user. $account = User::load($this->currentUser->id()); @@ -62,51 +62,52 @@ public function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $selection_handler_settings = $this->configuration['handler_settings']; + $form = parent::buildConfigurationForm($form, $form_state); + $selection_handler_settings = $this->configuration; // Merge-in default values. - $selection_handler_settings += array( + $selection_handler_settings += [ // For the 'target_bundles' setting, a NULL value is equivalent to "allow // entities from any bundle to be referenced" and an empty array value is // equivalent to "no entities from any bundle can be referenced". 'target_bundles' => NULL, - 'sort' => array( + 'sort' => [ 'field' => 'weight', 'direction' => 'ASC', - ), + ], 'auto_create' => FALSE, 'default_selection' => 'current', - ); + ]; - $form['target_bundles'] = array( + $form['target_bundles'] = [ '#type' => 'value', '#value' => NULL, - ); + ]; - $fields = array( + $fields = [ 'weight' => $this->t('Weight'), 'label' => $this->t('Name'), 'hostname' => $this->t('Hostname'), - ); + ]; - $form['sort']['field'] = array( + $form['sort']['field'] = [ '#type' => 'select', '#title' => $this->t('Sort by'), '#options' => $fields, '#ajax' => FALSE, '#default_value' => $selection_handler_settings['sort']['field'], - ); + ]; - $form['sort']['direction'] = array( + $form['sort']['direction'] = [ '#type' => 'select', '#title' => $this->t('Sort direction'), '#required' => TRUE, - '#options' => array( + '#options' => [ 'ASC' => $this->t('Ascending'), 'DESC' => $this->t('Descending'), - ), + ], '#default_value' => $selection_handler_settings['sort']['direction'], - ); + ]; return $form; } diff --git a/domain/src/Plugin/migrate/source/d7/DomainRecord.php b/domain/src/Plugin/migrate/source/d7/DomainRecord.php new file mode 100644 index 00000000..2b2945c9 --- /dev/null +++ b/domain/src/Plugin/migrate/source/d7/DomainRecord.php @@ -0,0 +1,57 @@ +select('domain', 'd')->fields('d', $fields); + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'domain_id' => $this->t('Domain ID.'), + 'subdomain' => $this->t('Subdomain.'), + 'sitename' => $this->t('Sitename.'), + 'scheme' => $this->t('Scheme.'), + 'valid' => $this->t('Valid.'), + 'weight' => $this->t('Weight.'), + 'is_default' => $this->t('Is default.'), + 'machine_name' => $this->t('Machine name.'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + return ['domain_id' => ['type' => 'integer']]; + } + +} diff --git a/domain/src/Plugin/views/access/Domain.php b/domain/src/Plugin/views/access/Domain.php index be50ebce..69ae30ab 100644 --- a/domain/src/Plugin/views/access/Domain.php +++ b/domain/src/Plugin/views/access/Domain.php @@ -6,9 +6,9 @@ use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\domain\DomainNegotiatorInterface; +use Drupal\domain\DomainStorageInterface; use Drupal\views\Plugin\views\access\AccessPluginBase; -use Drupal\domain\DomainLoader; -use Drupal\domain\DomainNegotiator; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\Route; @@ -31,9 +31,9 @@ class Domain extends AccessPluginBase implements CacheableDependencyInterface { /** * Domain storage. * - * @var \Drupal\domain\DomainLoader + * @var \Drupal\domain\DomainStorageInterface */ - protected $domainLoader; + protected $domainStorage; /** * Domain negotiation. @@ -51,14 +51,14 @@ class Domain extends AccessPluginBase implements CacheableDependencyInterface { * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. - * @param DomainLoader $domain_loader + * @param \Drupal\domain\DomainStorageInterface $domain_storage * The domain storage loader. - * @param DomainNegotiator $domain_negotiator + * @param \Drupal\domain\DomainNegotiatorInterface $domain_negotiator * The domain negotiator. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, DomainLoader $domain_loader, DomainNegotiator $domain_negotiator) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, DomainStorageInterface $domain_storage, DomainNegotiatorInterface $domain_negotiator) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->domainLoader = $domain_loader; + $this->domainStorage = $domain_storage; $this->domainNegotiator = $domain_negotiator; } @@ -70,7 +70,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('domain.loader'), + $container->get('entity_type.manager')->getStorage('domain'), $container->get('domain.negotiator') ); } @@ -93,6 +93,9 @@ public function alterRouteDefinition(Route $route) { } } + /** + * {@inheritdoc} + */ public function summaryTitle() { $count = count($this->options['domain']); if ($count < 1) { @@ -102,40 +105,48 @@ public function summaryTitle() { return $this->t('Multiple domains'); } else { - $domains = $this->domainLoader->loadOptionsList(); + $domains = $this->domainStorage->loadOptionsList(); $domain = reset($this->options['domain']); return $domains[$domain]; } } - + /** + * {@inheritdoc} + */ protected function defineOptions() { $options = parent::defineOptions(); - $options['domain'] = array('default' => array()); + $options['domain'] = ['default' => []]; return $options; } + /** + * {@inheritdoc} + */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - $form['domain'] = array( + $form['domain'] = [ '#type' => 'checkboxes', '#title' => $this->t('Domain'), '#default_value' => $this->options['domain'], - '#options' => $this->domainLoader->loadOptionsList(), + '#options' => $this->domainStorage->loadOptionsList(), '#description' => $this->t('Only the checked domain(s) will be able to access this display.'), - ); + ]; } + /** + * {@inheritdoc} + */ public function validateOptionsForm(&$form, FormStateInterface $form_state) { - $domain = $form_state->getValue(array('access_options', 'domain')); + $domain = $form_state->getValue(['access_options', 'domain']); $domain = array_filter($domain); if (!$domain) { $form_state->setError($form['domain'], $this->t('You must select at least one domain if type is "by domain"')); } - $form_state->setValue(array('access_options', 'domain'), $domain); + $form_state->setValue(['access_options', 'domain'], $domain); } /** @@ -145,7 +156,7 @@ public function calculateDependencies() { $dependencies = parent::calculateDependencies(); foreach (array_keys($this->options['domain']) as $id) { - if ($domain = $this->domainLoader->load($id)) { + if ($domain = $this->domainStorage->load($id)) { $dependencies[$domain->getConfigDependencyKey()][] = $domain->getConfigDependencyName(); } } diff --git a/domain/src/Plugin/views/argument_default/Domain.php b/domain/src/Plugin/views/argument_default/Domain.php new file mode 100644 index 00000000..89adb549 --- /dev/null +++ b/domain/src/Plugin/views/argument_default/Domain.php @@ -0,0 +1,78 @@ +domainNegotiator = $domain_negotiator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('domain.negotiator') + ); + } + + /** + * {@inheritdoc} + */ + public function getArgument() { + return $this->domainNegotiator->getActiveId(); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['url.site']; + } + +} diff --git a/domain/src/Routing/DomainRouteProvider.php b/domain/src/Routing/DomainRouteProvider.php new file mode 100644 index 00000000..0eda34ae --- /dev/null +++ b/domain/src/Routing/DomainRouteProvider.php @@ -0,0 +1,65 @@ +innerService = $inner_service; + parent::__construct($connection, $state, $current_path, $cache_backend, $path_processor, $cache_tag_invalidator, $table, $language_manager); + } + + /** + * Returns the cache ID for the route collection cache. + * + * We are overriding the cache id by inserting the host to the cid. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * + * @see \Drupal\Core\Routing\RouteProvider::getRouteCollectionCacheId() + * + * @return string + * The cache ID. + */ + protected function getRouteCollectionCacheId(Request $request) { + // Include the current language code in the cache identifier as + // the language information can be elsewhere than in the path, for example + // based on the domain. + $language_part = $this->getCurrentLanguageCacheIdPart(); + return 'route:' . $request->getHost() . ':' . $language_part . ':' . $request->getPathInfo() . ':' . $request->getQueryString(); + } + +} diff --git a/domain/src/Tests/DomainActionsTest.php b/domain/src/Tests/DomainActionsTest.php deleted file mode 100644 index 9c3cf41f..00000000 --- a/domain/src/Tests/DomainActionsTest.php +++ /dev/null @@ -1,60 +0,0 @@ -admin_user = $this->drupalCreateUser(array('administer domains', 'access administration pages')); - $this->drupalLogin($this->admin_user); - - $path = 'admin/config/domain'; - - // No domains should exist. - $this->domainTableIsEmpty(); - - // Create test domains. - $this->domainCreateTestDomains(4); - - // Visit the domain overview administration page. - $this->drupalGet($path); - $this->assertResponse(200); - - // Test the domains. - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - $this->assertTrue(count($domains) == 4, 'Four domain records found.'); - - // Check the default domain. - $default = \Drupal::service('domain.loader')->loadDefaultId(); - $key = 'example_com'; - $this->assertTrue($default == $key, 'Default domain set correctly.'); - - // Test some text on the page. - foreach ($domains as $domain) { - $name = $domain->label(); - $this->assertText($name, new FormattableMarkup('@name found on overview page.', array('@name' => $name))); - } - // @TODO: Test the list of actions. - $actions = array('delete', 'disable', 'default'); - foreach ($actions as $action) { - $this->assertRaw("/domain/{$action}/", new FormattableMarkup('@action action found.', array('@action' => $action))); - } - // @TODO: Disable a domain and test the enable link. - - // @TODO: test the link behaviors. - - // @TODO: test permissions on actions - - } - -} - diff --git a/domain/src/Tests/DomainCreateTest.php b/domain/src/Tests/DomainCreateTest.php deleted file mode 100644 index 93dacdce..00000000 --- a/domain/src/Tests/DomainCreateTest.php +++ /dev/null @@ -1,51 +0,0 @@ -domainTableIsEmpty(); - - // Create a new domain programmatically. - $domain = \Drupal::service('domain.creator')->createDomain(); - foreach (array('id', 'name', 'hostname', 'scheme', 'status', 'weight' , 'is_default') as $key) { - $property = $domain->get($key); - $this->assertTrue(isset($property), new FormattableMarkup('New $domain->@key property is set to default value: %value.', array('@key' => $key, '%value' => $property))); - } - $domain->save(); - - // Did it save correctly? - $default_id = \Drupal::service('domain.loader')->loadDefaultId(); - $this->assertTrue(!empty($default_id), 'Default domain has been set.'); - - // Does it load correctly? - $new_domain = \Drupal::service('domain.loader')->load($default_id); - $this->assertTrue($new_domain->id() == $domain->id(), 'Domain loaded properly.'); - - // Has domain id been set? - $this->assertTrue($new_domain->getDomainId(), 'Domain id set properly.'); - - // Has a UUID been set? - $this->assertTrue($new_domain->uuid(), 'Entity UUID set properly.'); - - // Delete the domain. - $domain->delete(); - $domain = \Drupal::service('domain.loader')->load($default_id, TRUE); - $this->assertTrue(empty($domain), 'Domain record deleted.'); - - // No domains should exist. - $this->domainTableIsEmpty(); - } - -} diff --git a/domain/src/Tests/DomainHooksTest.php b/domain/src/Tests/DomainHooksTest.php deleted file mode 100644 index fa917c3f..00000000 --- a/domain/src/Tests/DomainHooksTest.php +++ /dev/null @@ -1,48 +0,0 @@ -domainTableIsEmpty(); - - // Create a domain. - $this->domainCreateTestDomains(); - - // Check the created domain based on it's known id value. - $key = 'example_com'; - - $domain = \Drupal::service('domain.loader')->load($key); - - // Internal hooks. - $path = $domain->getPath(); - $url = $domain->getUrl(); - $this->assertTrue(isset($path), new FormattableMarkup('The path property was set to %path by hook_entity_load.', array('%path' => $path))); - $this->assertTrue(isset($url), new FormattableMarkup('The url property was set to %url by hook_entity_load.', array('%url' => $url))); - - // External hooks. - $this->assertTrue($domain->foo == 'bar', 'The foo property was set to bar by hook_domain_load.'); - - // @TODO: test additional hooks. - } - -} diff --git a/domain/src/Tests/DomainTestBase.php b/domain/src/Tests/DomainTestBase.php deleted file mode 100644 index 7dc6d070..00000000 --- a/domain/src/Tests/DomainTestBase.php +++ /dev/null @@ -1,203 +0,0 @@ -base_hostname or the - * domainCreateTestDomains() method. - */ - public $base_hostname; - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array('domain', 'node'); - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Create Basic page and Article node types. - if ($this->profile != 'standard') { - $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); - } - - // Set the base hostname for domains. - $this->base_hostname = \Drupal::service('domain.creator')->createHostname(); - } - - /** - * Reusable test function for checking initial / empty table status. - */ - public function domainTableIsEmpty() { - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - $this->assertTrue(empty($domains), 'No domains have been created.'); - $default_id = \Drupal::service('domain.loader')->loadDefaultId(); - $this->assertTrue(empty($default_id), 'No default domain has been set.'); - } - - /** - * Creates domain record for use with POST request tests. - */ - public function domainPostValues() { - $edit = array(); - $domain = \Drupal::service('domain.creator')->createDomain(); - $required = \Drupal::service('domain.validator')->getRequiredFields(); - foreach ($required as $key) { - $edit[$key] = $domain->get($key); - } - return $edit; - } - - /** - * Generates a list of domains for testing. - * - * In my environment, I use the example.com hostname as a base. Then I name - * hostnames one.* two.* up to ten. Note that we always use *_example_com - * for the machine_name (entity id) value, though the hostname can vary - * based on the system. This naming allows us to load test schema files. - * - * The script may also add test1, test2, test3 up to any number to test a - * large number of domains. - * - * @param int $count - * The number of domains to create. - * @param string|NULL $base_hostname - * The root domain to use for domain creation (e.g. example.com). - * @param array $list - * An optional list of subdomains to apply instead of the default set. - */ - public function domainCreateTestDomains($count = 1, $base_hostname = NULL, $list = array()) { - $original_domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - if (empty($base_hostname)) { - $base_hostname = $this->base_hostname; - } - // Note: these domains are rigged to work on my test server. - // For proper testing, yours should be set up similarly, but you can pass a - // $list array to change the default. - if (empty($list)) { - $list = array('', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'); - } - for ($i = 0; $i < $count; $i++) { - if (!empty($list[$i])) { - if ($i < 11) { - $hostname = $list[$i] . '.' . $base_hostname; - $machine_name = $list[$i] . '.example.com'; - $name = ucfirst($list[$i]); - } - // These domains are not setup and are just for UX testing. - else { - $hostname = 'test' . $i . '.' . $base_hostname; - $machine_name = 'test' . $i . '.example.com'; - $name = 'Test ' . $i; - } - } - else { - $hostname = $base_hostname; - $machine_name = 'example.com'; - $name = 'Example'; - } - // Create a new domain programmatically. - $values = array( - 'hostname' => $hostname, - 'name' => $name, - 'id' => \Drupal::service('domain.creator')->createMachineName($machine_name), - ); - $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); - $domain->save(); - } - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - $this->assertTrue((count($domains) - count($original_domains)) == $count, new FormattableMarkup('Created %count new domains.', array('%count' => $count))); - } - - /** - * Returns whether a given user account is logged in. - * - * @param \Drupal\user\UserInterface $account - * The user account object to check. - * - * @return bool - */ - protected function drupalUserIsLoggedIn($account) { - // @TODO: This is a temporary hack for the test login fails when setting $cookie_domain. - if (!isset($account->session_id)) { - return (bool) $account->id(); - } - // The session ID is hashed before being stored in the database. - // @see \Drupal\Core\Session\SessionHandler::read() - return (bool) db_query("SELECT sid FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => Crypt::hashBase64($account->session_id)))->fetchField(); - } - - /** - * Adds a test domain to an entity. - * - * @param string $entity_type - * The entity type being acted upon. - * @param int $entity_id - * The entity id. - * @param int $id - * The id of the domain to add. - * @param string $field - * The name of the domain field used to attach to the entity. - */ - public function addDomainToEntity($entity_type, $entity_id, $id, $field = DOMAIN_ACCESS_FIELD) { - if ($entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) { - $entity->set($field, $id); - $entity->save(); - } - } - - /** - * Login a user on a specific domain. - * - * @param \Drupal\domain\DomainInterface $domain - * The domain to log the user into. - * @param \Drupal\user\UserInterface $account - * The user account to login. - */ - public function domainLogin(DomainInterface $domain, UserInterface $account) { - if ($this->loggedInUser) { - $this->drupalLogout(); - } - - // For this to work, we must reset the password to a known value. - $pass = 'thisissatestpassword'; - /** @var UserInterface $user */ - $user = \Drupal::entityTypeManager()->getStorage('user')->load($account->id()); - $user->setPassword($pass)->save(); - $url = $domain->getPath() . 'user/login'; - $edit = ['name' => $account->getAccountName(), 'pass' => $pass]; - $this->drupalPostForm($url, $edit, t('Log in')); - - // @see WebTestBase::drupalUserIsLoggedIn() - if (isset($this->sessionId)) { - $account->session_id = $this->sessionId; - } - $pass = $this->assert($this->drupalUserIsLoggedIn($account), new FormattableMarkup('User %name successfully logged in.', array('%name' => $account->getUsername())), 'User login'); - if ($pass) { - $this->loggedInUser = $account; - $this->container->get('current_user')->setAccount($account); - } - } - -} diff --git a/domain/src/Tests/DomainValidatorTest.php b/domain/src/Tests/DomainValidatorTest.php deleted file mode 100644 index a84d690f..00000000 --- a/domain/src/Tests/DomainValidatorTest.php +++ /dev/null @@ -1,77 +0,0 @@ -domainTableIsEmpty(); - $creator = \Drupal::service('domain.creator'); - $validator = \Drupal::service('domain.validator'); - - // Create a domain. - $this->domainCreateTestDomains(1, 'foo.com'); - // Check the created domain based on its known id value. - $key = 'foo.com'; - /** @var \Drupal\domain\Entity\Domain $domain */ - $domain = \Drupal::service('domain.loader')->loadByHostname($key); - $this->assertTrue(!empty($domain), 'Test domain created.'); - - // Valid hostnames to test. Valid is the boolean value. - $hostnames = [ - 'localhost' => 1, - 'example.com' => 1, - 'www.example.com' => 1, // see www-prefix test, below. - 'one.example.com' => 1, - 'example.com:8080' => 1, - 'example.com::8080' => 0, // only one colon. - 'example.com:abc' => 0, // no letters after a colon. - '.example.com' => 0, // cannot begin with a dot. - 'example.com.' => 0, // cannot end with a dot. - 'EXAMPLE.com' => 0, // lowercase only. - 'éxample.com' => 0, // ascii-only. - 'foo.com' => 0, // duplicate. - ]; - foreach ($hostnames as $hostname => $valid) { - $domain = $creator->createDomain(['hostname' => $hostname]); - $errors = $validator->validate($domain); - if ($valid) { - $this->assertTrue(empty($errors), new FormattableMarkup('Validation test for @hostname passed.', array('@hostname' => $hostname))); - } - else { - $this->assertTrue(!empty($errors), new FormattableMarkup('Validation test for @hostname failed.', array('@hostname' => $hostname))); - } - } - // Test the two configurable options. - $config = $this->config('domain.settings'); - $config->set('www_prefix', true)->save(); - $config->set('allow_non_ascii', true)->save(); - // Valid hostnames to test. Valid is the boolean value. - $hostnames = [ - 'www.example.com' => 0, // no www-prefix allowed - 'éxample.com' => 1, // ascii-only allowed. - ]; - foreach ($hostnames as $hostname => $valid) { - $domain = $creator->createDomain(['hostname' => $hostname]); - $errors = $validator->validate($domain); - if ($valid) { - $this->assertTrue(empty($errors), new FormattableMarkup('Validation test for @hostname passed.', array('@hostname' => $hostname))); - } - else { - $this->assertTrue(!empty($errors), new FormattableMarkup('Validation test for @hostname failed.', array('@hostname' => $hostname))); - } - } - - } - -} diff --git a/domain/templates/domain-nav-block.html.twig b/domain/templates/domain-nav-block.html.twig new file mode 100644 index 00000000..d2d4c0ac --- /dev/null +++ b/domain/templates/domain-nav-block.html.twig @@ -0,0 +1,32 @@ +{# +/** + * @file + * Custom theme implementation for a select-and-go menu list. + * + * Available variables: + * - items: A list of items containing an array of strings. + * - label: the label text + * - url: the link url string + * - active: indicates if the domain is currently active. + * + * @ingroup themeable + */ +#} +{%- if items -%} +
+
+ +
+
+{%- else -%} + {{- empty -}} +{%- endif -%} diff --git a/domain/tests/modules/domain_config_schema_test/config/install/domain.record.drupal.yml b/domain/tests/modules/domain_config_schema_test/config/install/domain.record.drupal.yml new file mode 100644 index 00000000..57d74edc --- /dev/null +++ b/domain/tests/modules/domain_config_schema_test/config/install/domain.record.drupal.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: { } +id: drupal +domain_id: 4308242 +hostname: drupal.org +name: 'Drupal.org Domain Test' +scheme: http +weight: 1 +is_default: true diff --git a/domain/tests/modules/domain_config_schema_test/domain_config_schema_test.info.yml b/domain/tests/modules/domain_config_schema_test/domain_config_schema_test.info.yml new file mode 100644 index 00000000..516db474 --- /dev/null +++ b/domain/tests/modules/domain_config_schema_test/domain_config_schema_test.info.yml @@ -0,0 +1,15 @@ +name: "Domain module config schema tests" +description: "Support module for domain config schema testing." +type: module +package: Testing +# version: VERSION +core_version_requirement: ^8 || ^9 +hidden: TRUE + +dependencies: + - domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain/tests/modules/domain_test/config/optional/views.view.domain_views_access.yml b/domain/tests/modules/domain_test/config/optional/views.view.domain_views_access.yml index 148f0ff5..23d57e78 100644 --- a/domain/tests/modules/domain_test/config/optional/views.view.domain_views_access.yml +++ b/domain/tests/modules/domain_test/config/optional/views.view.domain_views_access.yml @@ -107,7 +107,7 @@ display: field_api_classes: false filters: status: - value: true + value: '1' table: users_field_data field: status plugin_id: boolean diff --git a/domain/tests/modules/domain_test/domain_test.info.yml b/domain/tests/modules/domain_test/domain_test.info.yml index 114d8918..79a824b5 100644 --- a/domain/tests/modules/domain_test/domain_test.info.yml +++ b/domain/tests/modules/domain_test/domain_test.info.yml @@ -2,9 +2,14 @@ name: "Domain module hook tests" description: "Support module for domain hook testing." type: module package: Testing -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 hidden: TRUE dependencies: - domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain/tests/modules/domain_test/domain_test.module b/domain/tests/modules/domain_test/domain_test.module index 8e6ba8bd..b46c1c2f 100644 --- a/domain/tests/modules/domain_test/domain_test.module +++ b/domain/tests/modules/domain_test/domain_test.module @@ -5,6 +5,9 @@ * Domain hook test module. */ +use Drupal\Core\Url; +use Drupal\domain\DomainInterface; + /** * Implements hook_domain_load(). */ @@ -13,3 +16,45 @@ function domain_test_domain_load(array $domains) { $domain->addProperty('foo', 'bar'); } } + +/** + * Implements hook_domain_validate_alter(). + */ +function domain_test_domain_validate_alter(&$error_list, $hostname) { + // Deliberate test fail. + if ($hostname == 'fail.example.com') { + $error_list[] = 'Fail.example.com cannot be registered'; + } +} + +/** + * Implements hook_domain_request_alter(). + */ +function domain_test_domain_request_alter(DomainInterface &$domain) { + $domain->addProperty('foo1', 'bar1'); +} + +/** + * Implements hook_domain_operations(). + */ +function domain_test_domain_operations(DomainInterface $domain) { + $operations = []; + // Add aliases to the list. + $id = $domain->id(); + $operations['domain_test'] = [ + 'title' => t('Test'), + 'url' => Url::fromRoute('entity.domain.edit_form', ['domain' => $id]), + 'weight' => 80, + ]; + return $operations; +} + +/** + * Implements hook_domain_references_alter(). + */ +function domain_test_domain_references_alter($query, $account, $context) { + if ($context['entity_type'] == 'node') { + $test = 'Test string'; + $query->addMetadata('domain_test', $test); + } +} diff --git a/domain/tests/modules/domain_test_views/config/optional/views.view.test_active_domain_argument.yml b/domain/tests/modules/domain_test_views/config/optional/views.view.test_active_domain_argument.yml new file mode 100644 index 00000000..f1a05365 --- /dev/null +++ b/domain/tests/modules/domain_test_views/config/optional/views.view.test_active_domain_argument.yml @@ -0,0 +1,286 @@ +langcode: en +status: true +dependencies: + module: + - domain_access + - node + - user +id: test_active_domain_argument +label: test_active_domain_argument +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: table + row: + type: fields + fields: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + label: ID + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + operator_limit_selection: false + operator_list: { } + group: 1 + sorts: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: nid + plugin_id: standard + title: test_active_domain_argument + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + field_domain_access_target_id: + id: field_domain_access_target_id + table: node__field_domain_access + field: field_domain_access_target_id + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: active_domain + default_argument_options: { } + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + plugin_id: domain_access_argument + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - url.site + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-active-domain-argument + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - url.site + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/domain/tests/modules/domain_test_views/domain_test_views.info.yml b/domain/tests/modules/domain_test_views/domain_test_views.info.yml new file mode 100644 index 00000000..401f9193 --- /dev/null +++ b/domain/tests/modules/domain_test_views/domain_test_views.info.yml @@ -0,0 +1,16 @@ +name: 'Domain test views' +type: module +description: 'Provides default views for views domain tests.' +package: Testing +# version: VERSION +core_version_requirement: ^8 || ^9 +dependencies: + - drupal:node + - drupal:views + - drupal:language + - domain:domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain/src/Tests/Condition/DomainConditionTest.php b/domain/tests/src/Functional/Condition/DomainConditionTest.php similarity index 57% rename from domain/src/Tests/Condition/DomainConditionTest.php rename to domain/tests/src/Functional/Condition/DomainConditionTest.php index de75a252..8096918d 100644 --- a/domain/src/Tests/Condition/DomainConditionTest.php +++ b/domain/tests/src/Functional/Condition/DomainConditionTest.php @@ -1,8 +1,8 @@ domainCreateTestDomains(5); // Get two sample domains. - $this->domains = \Drupal::service('domain.loader')->loadMultiple(); - $this->test_domain = array_shift($this->domains); - $this->not_domain = array_shift($this->domains); + $this->domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $this->testDomain = array_shift($this->domains); + $this->notDomain = array_shift($this->domains); } /** @@ -56,30 +62,28 @@ public function setUp() { */ public function testConditions() { // Grab the domain condition and configure it to check against one domain. - $condition = $this->manager->createInstance('domain') - ->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id())) - ->setContextValue('entity:domain', $this->not_domain); + $configuration = [ + 'domains' => [$this->testDomain->id() => $this->testDomain->id()], + 'context' => ['domain' => $this->notDomain], + ]; + $condition = $this->manager->createInstance('domain', $configuration); $this->assertFalse($condition->execute(), 'Domain request condition fails on wrong domain.'); - // Grab the domain condition and configure it to check against a null set. - $condition = $this->manager->createInstance('domain') - ->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id())) - ->setContextValue('entity:domain', NULL); - $this->assertFalse($condition->execute(), 'Domain request condition fails when no context present.'); - // Grab the domain condition and configure it to check against itself. - $condition = $this->manager->createInstance('domain') - ->setConfig('domains', array($this->test_domain->id() => $this->test_domain->id())) - ->setContextValue('entity:domain', $this->test_domain); + $configuration = [ + 'domains' => [$this->testDomain->id() => $this->testDomain->id()], + 'context' => ['domain' => $this->testDomain], + ]; + $condition = $this->manager->createInstance('domain', $configuration); $this->assertTrue($condition->execute(), 'Domain request condition succeeds on matching domain.'); // Check for the proper summary. // Summaries require an extra space due to negate handling in summary(). - $this->assertEqual($condition->summary(), 'Active domain is ' . $this->test_domain->label()); + $this->assertEqual($condition->summary(), 'Active domain is ' . $this->testDomain->label()); // Check the negated summary. $condition->setConfig('negate', TRUE); - $this->assertEqual($condition->summary(), 'Active domain is not ' . $this->test_domain->label()); + $this->assertEqual($condition->summary(), 'Active domain is not ' . $this->testDomain->label()); // Check the negated condition. $this->assertFalse($condition->execute(), 'Domain request condition fails when condition negated.'); diff --git a/domain/tests/src/Functional/DomainActionsTest.php b/domain/tests/src/Functional/DomainActionsTest.php new file mode 100644 index 00000000..b170f550 --- /dev/null +++ b/domain/tests/src/Functional/DomainActionsTest.php @@ -0,0 +1,100 @@ +admin_user = $this->drupalCreateUser(['administer domains', 'access administration pages']); + $this->drupalLogin($this->admin_user); + + $path = 'admin/config/domain'; + + // Create test domains. + $this->domainCreateTestDomains(4); + + // Visit the domain overview administration page. + $this->drupalGet($path); + $this->assertResponse(200); + + // Test the domains. + $storage = \Drupal::entityTypeManager()->getStorage('domain'); + $domains = $storage->loadMultiple(); + $this->assertCount(4, $domains, 'Four domain records found.'); + + // Check the default domain. + $default = $storage->loadDefaultId(); + $key = 'example_com'; + $this->assertTrue($default == $key, 'Default domain set correctly.'); + + // Test some text on the page. + foreach ($domains as $domain) { + $name = $domain->label(); + $this->assertText($name, 'Name found properly.'); + } + // Test the list of actions. + $actions = ['delete', 'disable', 'default']; + foreach ($actions as $action) { + $this->assertRaw("/domain/{$action}/", 'Actions found properly.'); + } + // Check that all domains are active. + $this->assertNoRaw('Inactive', 'Inactive domain not found.'); + + // Disable a domain and test the enable link. + $this->clickLink('Disable', 0); + $this->assertRaw('Inactive', 'Inactive domain found.'); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + foreach ($storage->loadMultiple() as $domain) { + if ($domain->id() == 'one_example_com') { + $this->assertEmpty($domain->status(), 'One domain inactive.'); + } + else { + $this->assertNotEmpty($domain->status(), 'Other domains active.'); + } + } + + // Test the list of actions. + $actions = ['enable', 'delete', 'disable', 'default']; + foreach ($actions as $action) { + $this->assertRaw("/domain/{$action}/", 'Actions found properly.'); + } + // Re-enable the domain. + $this->clickLink('Enable', 0); + $this->assertNoRaw('Inactive', 'Inactive domain not found.'); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + foreach ($storage->loadMultiple() as $domain) { + $this->assertNotEmpty($domain->status(), 'All domains active.'); + } + + // Set a new default domain. + $this->clickLink('Make default', 0); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + // Check the default domain. + $storage->resetCache(); + $default = $storage->loadDefaultId(); + $key = 'one_example_com'; + $this->assertTrue($default == $key, 'Default domain set correctly.'); + + } + +} diff --git a/domain/tests/src/Functional/DomainAdminElementTest.php b/domain/tests/src/Functional/DomainAdminElementTest.php index fbd5343f..59b54c9c 100644 --- a/domain/tests/src/Functional/DomainAdminElementTest.php +++ b/domain/tests/src/Functional/DomainAdminElementTest.php @@ -2,7 +2,8 @@ namespace Drupal\Tests\domain\Functional; -use Drupal\Tests\domain\Functional\DomainTestBase; +use Drupal\domain\DomainInterface; +use Drupal\user\UserInterface; /** * Tests behavior for the domain admin field element. @@ -16,7 +17,7 @@ class DomainAdminElementTest extends DomainTestBase { * * @var array */ - public static $modules = array('domain', 'field', 'field_ui', 'user'); + public static $modules = ['domain', 'field', 'field_ui', 'user']; /** * {@inheritdoc} @@ -32,12 +33,12 @@ protected function setUp() { * Basic test setup. */ public function testDomainAccessElement() { - $admin = $this->drupalCreateUser(array( + $admin = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer users', 'administer domains', - )); + ]); $this->drupalLogin($admin); $this->drupalGet('admin/people/create'); @@ -50,11 +51,11 @@ public function testDomainAccessElement() { $this->fillField('pass[pass2]', 'test'); // We expect to find 5 domain options. We set two as selected. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); $count = 0; $ids = ['example_com', 'one_example_com', 'two_example_com']; foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if (in_array($domain->id(), $ids)) { $this->checkField($locator); @@ -69,27 +70,27 @@ public function testDomainAccessElement() { $user = $storage->load(3); // Check that two values are set. $manager = \Drupal::service('domain.element_manager'); - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 3, 'User saved with three domain records.'); // Now login as a user with limited rights. - $account = $this->drupalCreateUser(array( + $account = $this->drupalCreateUser([ 'administer users', 'assign domain administrators', - )); + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $account->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); $tester = $storage->load($account->id()); - $values = $manager->getFieldValues($tester, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($tester, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 2, 'User saved with two domain records.'); - $storage->resetCache(array($account->id())); + $storage->resetCache([$account->id()]); $this->drupalLogin($account); $this->drupalGet('user/' . $user->id() . '/edit'); $this->assertSession()->statusCodeEquals(200); foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if ($domain->id() == 'example_com') { $this->checkField($locator); @@ -107,30 +108,33 @@ public function testDomainAccessElement() { $this->assertSession()->statusCodeEquals(200); // Now, check the user. - $storage->resetCache(array($user->id())); + $storage->resetCache([$user->id()]); $user = $storage->load($user->id()); // Check that two values are set. - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 2, 'User saved with two domain records.'); // Test the case presented in https://www.drupal.org/node/2841962. $config = \Drupal::configFactory()->getEditable('user.settings'); $config->set('verify_mail', 0); - $config->set('register', USER_REGISTER_VISITORS); + $config->set('register', UserInterface::REGISTER_VISITORS); $config->save(); $this->drupalLogout(); $this->drupalGet('user/register'); $this->assertSession()->statusCodeEquals(200); $this->assertSession()->responseNotContains('Domain administrator'); foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->assertSession()->fieldNotExists($locator); } // Create a user through the form. $this->fillField('name', 'testuser2'); $this->fillField('mail', 'test2@example.com'); - $this->fillField('pass[pass1]', 'test'); - $this->fillField('pass[pass2]', 'test'); + // In 8.3, this field is not present? + if (!empty($this->findField('pass[pass1]'))) { + $this->fillField('pass[pass1]', 'test'); + $this->fillField('pass[pass2]', 'test'); + } // Save the form. $this->pressButton('edit-submit'); $this->assertSession()->statusCodeEquals(200); diff --git a/domain/tests/src/Functional/DomainBlockVisibilityTest.php b/domain/tests/src/Functional/DomainBlockVisibilityTest.php new file mode 100644 index 00000000..ef8fb101 --- /dev/null +++ b/domain/tests/src/Functional/DomainBlockVisibilityTest.php @@ -0,0 +1,94 @@ +domainCreateTestDomains(4); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + + // Place the nav block. + $block = $this->placeBlock('domain_nav_block'); + + // Let the anon user view the block. + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['use domain nav block']); + + // Load the homepage. All links should appear. + foreach ($domains as $domain) { + $url = $domain->getPath(); + $this->drupalGet($url); + $this->assertBlockAppears($block); + } + + // Now let's only show the block on two domains. + $allowed_domains = [ + 'example_com' => 'example_com', + 'one_example_com' => 'one_example_com', + ]; + $settings = [ + 'visibility' => [ + 'domain' => [ + 'id' => 'domain', + 'domains' => $allowed_domains, + 'negate' => FALSE, + 'context_mapping' => ['domain' => '@domain.current_domain_context:domain'], + ], + ], + ]; + $block2 = $this->placeBlock('domain_nav_block', $settings); + + // Load the homepage. All links should appear. + foreach ($domains as $id => $domain) { + $url = $domain->getPath(); + $this->drupalGet($url); + if (in_array($id, $allowed_domains, TRUE)) { + $this->assertBlockAppears($block2); + } + else { + $this->assertNoBlockAppears($block2); + } + } + + // Now let's negate (reverse) the condition. + $settings['visibility']['domain']['negate'] = TRUE; + $block3 = $this->placeBlock('domain_nav_block', $settings); + + // Load the homepage. All links should appear. + foreach ($domains as $id => $domain) { + $url = $domain->getPath(); + $this->drupalGet($url); + if (!in_array($id, $allowed_domains, TRUE)) { + $this->assertBlockAppears($block3); + } + else { + $this->assertNoBlockAppears($block3); + } + } + + } + +} diff --git a/domain/src/Tests/DomainCSSTest.php b/domain/tests/src/Functional/DomainCSSTest.php similarity index 58% rename from domain/src/Tests/DomainCSSTest.php rename to domain/tests/src/Functional/DomainCSSTest.php index fc092389..853e4919 100644 --- a/domain/src/Tests/DomainCSSTest.php +++ b/domain/tests/src/Functional/DomainCSSTest.php @@ -1,6 +1,6 @@ install(array('bartik')); + \Drupal::service('theme_installer')->install(['bartik']); } /** @@ -39,7 +42,7 @@ public function testDomainNegotiator() { $config->set('default', 'bartik')->save(); // Test the response of the default home page. - foreach (\Drupal::service('domain.loader')->loadMultiple() as $domain) { + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultiple() as $domain) { $this->drupalGet($domain->getPath()); $text = 'id() . '-class'); $this->assertRaw($text, 'Custom CSS present.' . $text); } + + // Set the css classes. + $config = $this->config('domain.settings'); + $config->set('css_classes', '[domain:machine-name]-class [domain:name]-class')->save(); + // Test the response of the default home page. + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultiple() as $domain) { + // The render cache trips up this test. In production, it may be + // necessary to add the url.site cache context. See README.md. + drupal_flush_all_caches(); + $this->drupalGet($domain->getPath()); + $text = 'id() . '"'; - $this->assertRaw($string, new FormattableMarkup('Found the %domain option.', array('%domain' => $domain->label()))); + $this->assertRaw($string, 'Found the domain option'); if (!isset($one)) { $one = $domain->id(); continue; @@ -103,13 +105,14 @@ public function testDomainFieldStorage() { $edit['title[0][value]'] = 'Test node'; $edit["field_domain[{$one}]"] = TRUE; $edit["field_domain[{$two}]"] = TRUE; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertResponse(200); $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); $values = $node->get('field_domain'); // Get the expected value count. - $this->assertTrue(count($values) == 2, 'Node saved with two domain records.'); + $this->assertCount(2, $values, 'Node saved with two domain records.'); } @@ -122,38 +125,36 @@ public function domainCreateTestField() { $label = 'domain'; $name = 'field_' . $label; - $storage = array( + $storage = [ 'field_name' => $name, 'entity_type' => 'node', 'type' => 'entity_reference', 'cardinality' => -1, - 'settings' => array( + 'settings' => [ 'target_type' => 'domain', - ), - ); + ], + ]; $field_storage_config = \Drupal::entityTypeManager()->getStorage('field_storage_config')->create($storage); $field_storage_config->save(); - $field = array( + $field = [ 'field_name' => $name, 'entity_type' => 'node', 'label' => 'Domain test field', 'bundle' => 'article', - 'settings' => array( - 'handler_settings' => array( - 'sort' => array('field' => 'weight', 'direction' => 'ASC'), - ), - ), - ); + 'settings' => [ + 'handler_settings' => [ + 'sort' => ['field' => 'weight', 'direction' => 'ASC'], + ], + ], + ]; $field_config = \Drupal::entityTypeManager()->getStorage('field_config')->create($field); $field_config->save(); // Tell the form system how to behave. - entity_get_form_display('node', 'article', 'default') - ->setComponent($name, array( - 'type' => 'options_buttons', - )) - ->save(); + if ($display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load('node.article.default')) { + $display->setComponent($name, ['type' => 'options_buttons'])->save(); + } } } diff --git a/domain/src/Tests/DomainFormsTest.php b/domain/tests/src/Functional/DomainFormsTest.php similarity index 52% rename from domain/src/Tests/DomainFormsTest.php rename to domain/tests/src/Functional/DomainFormsTest.php index 7ea382f9..eceab646 100644 --- a/domain/src/Tests/DomainFormsTest.php +++ b/domain/tests/src/Functional/DomainFormsTest.php @@ -1,6 +1,6 @@ admin_user = $this->drupalCreateUser(array('administer domains', 'create domains')); + $this->admin_user = $this->drupalCreateUser(['administer domains', 'create domains']); $this->drupalLogin($this->admin_user); + $storage = \Drupal::entityTypeManager()->getStorage('domain'); + // No domains should exist. $this->domainTableIsEmpty(); @@ -23,36 +25,44 @@ public function testDomainInterface() { $this->drupalGet('admin/config/domain'); // Check for the add message. - $this->assertText('There is no Domain record yet.', 'Text for no domains found.'); + $this->assertText('There are no domain record entities yet.', 'Text for no domains found.'); + // Visit the add domain administration page. $this->drupalGet('admin/config/domain/add'); // Make a POST request on admin/config/domain/add. $edit = $this->domainPostValues(); - $this->drupalPostForm('admin/config/domain/add', $edit, 'Save'); + // Use hostname with dot (.) to avoid validation error. + $edit['hostname'] = 'example.com'; + $this->drupalGet('admin/config/domain/add'); + $this->submitForm($edit, 'Save'); // Did it save correctly? - $default_id = \Drupal::service('domain.loader')->loadDefaultId(); - $this->assertTrue(!empty($default_id), 'Domain record saved via form.'); + $default_id = $storage->loadDefaultId(); + $this->assertNotEmpty($default_id, 'Domain record saved via form.'); // Does it load correctly? - $new_domain = \Drupal::service('domain.loader')->load($default_id); - $this->assertTrue($new_domain->id() == $edit['id'], 'Domain loaded properly.'); + $storage->resetCache([$default_id]); + $new_domain = $storage->load($default_id); + $this->assertTrue($new_domain->id() == $default_id, 'Domain loaded properly.'); // Has a UUID been set? - $uuid = $new_domain->uuid(); - $this->assertTrue(!empty($uuid), 'Entity UUID set properly.'); + $this->assertNotEmpty($new_domain->uuid(), 'Entity UUID set properly.'); // Visit the edit domain administration page. $editUrl = 'admin/config/domain/edit/' . $new_domain->id(); $this->drupalGet($editUrl); // Update the record. + $edit = []; $edit['name'] = 'Foo'; - $this->drupalPostForm($editUrl, $edit, $this->t('Save')); + $edit['validate_url'] = 0; + $this->drupalGet($editUrl); + $this->submitForm($edit, 'Save'); // Check that the update succeeded. - $domain = \Drupal::service('domain.loader')->load($default_id, TRUE); + $storage->resetCache([$default_id]); + $domain = $storage->load($default_id); $this->assertTrue($domain->label() == 'Foo', 'Domain record updated via form.'); // Visit the delete domain administration page. @@ -60,9 +70,11 @@ public function testDomainInterface() { $this->drupalGet($deleteUrl); // Delete the record. - $this->drupalPostForm($deleteUrl, array(), $this->t('Delete')); - $domain = \Drupal::service('domain.loader')->load($default_id, TRUE); - $this->assertTrue(empty($domain), 'Domain record deleted.'); + $this->drupalGet($deleteUrl); + $this->submitForm([], 'Delete'); + $storage->resetCache([$default_id]); + $domain = $storage->load($default_id); + $this->assertEmpty($domain, 'Domain record deleted.'); // No domains should exist. $this->domainTableIsEmpty(); diff --git a/domain/src/Tests/DomainGetResponseTest.php b/domain/tests/src/Functional/DomainGetResponseTest.php similarity index 52% rename from domain/src/Tests/DomainGetResponseTest.php rename to domain/tests/src/Functional/DomainGetResponseTest.php index 43ed567d..35128bce 100644 --- a/domain/src/Tests/DomainGetResponseTest.php +++ b/domain/tests/src/Functional/DomainGetResponseTest.php @@ -1,8 +1,6 @@ domainCreateTestDomains(); - // Check the created domain based on it's known id value. + // Check the created domain based on its known id value. $key = 'example_com'; /** @var \Drupal\domain\Entity\Domain $domain */ - $domain = \Drupal::service('domain.loader')->load($key); + $domain = \Drupal::entityTypeManager()->getStorage('domain')->load($key); // Our testing server should be able to access the test PNG file. - $this->assertTrue($domain->getResponse() == 200, new FormattableMarkup('Server test for @url passed.', array('@url' => $domain->getPath()))); + $this->assert($domain->getResponse() == 200, 'Server returned a 200 response.'); // Now create a bad domain. - $values = array( + $values = [ 'hostname' => 'foo.bar', 'id' => 'foo_bar', 'name' => 'Foo', - ); - $domain = \Drupal::service('domain.creator')->createDomain($values); + ]; + $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); $domain->save(); - $this->assertTrue($domain->getResponse() == 500, new FormattableMarkup('Server test for @url failed.', array('@url' => $domain->getPath()))); + $this->assert($domain->getResponse() == 500, 'Server test returned a 500 response.'); } } diff --git a/domain/src/Tests/DomainInactiveTest.php b/domain/tests/src/Functional/DomainInactiveTest.php similarity index 51% rename from domain/src/Tests/DomainInactiveTest.php rename to domain/tests/src/Functional/DomainInactiveTest.php index 219a881f..b2af33a7 100644 --- a/domain/src/Tests/DomainInactiveTest.php +++ b/domain/tests/src/Functional/DomainInactiveTest.php @@ -1,8 +1,9 @@ config('system.site'); $site_config->set('page.front', '/node')->save(); - // Create three new domains programmatically. - $this->domainCreateTestDomains(3); - $domains = \Drupal::service('domain.loader')->loadMultiple(); + // Create four new domains programmatically. + $this->domainCreateTestDomains(4); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); - // Grab the last domain for testing. - $domain = end($domains); + // Grab a known domain for testing. + $domain = $domains['two_example_com']; $this->drupalGet($domain->getPath()); $this->assertTrue($domain->status(), 'Tested domain is set to active.'); $this->assertTrue($domain->getPath() == $this->getUrl(), 'Loaded the active domain.'); // Disable the domain and test for redirect. $domain->disable(); - $default = \Drupal::service('domain.loader')->loadDefaultDomain(); + $default = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultDomain(); // Our postSave() cache tag clear should allow this to work properly. $this->drupalGet($domain->getPath()); @@ -54,15 +55,44 @@ public function testInactiveDomain() { $this->drupalGet($url); $this->assertResponse(200, 'Request to reset password on inactive domain allowed.'); - // @TODO: configure more paths and test. - // Try to access with the proper permission. - user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('access inactive domains')); + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['access inactive domains']); // Must flush cache because we did not resave the domain. drupal_flush_all_caches(); $this->assertFalse($domain->status(), 'Tested domain is set to inactive.'); $this->drupalGet($domain->getPath()); - $this->assertTrue($domain->getPath() == $this->getUrl(), 'Loaded the inactive domain with permission.'); + + // Set up two additional domains. + $domain2 = $domains['one_example_com']; + $domain3 = $domains['three_example_com']; + + // Check against trusted host patterns. + $settings['settings']['trusted_host_patterns'] = (object) [ + 'value' => ['^' . $this->prepareTrustedHostname($domain->getHostname()) . '$', + '^' . $this->prepareTrustedHostname($domain2->getHostname()) . '$', + ], + 'required' => TRUE, + ]; + $this->writeSettings($settings); + + // Revoke the permission change. + user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, ['access inactive domains']); + + $domain2->saveDefault(); + + // Test the trusted host, which should redirect to default. + $this->drupalGet($domain->getPath()); + $this->assertTrue($domain2->getPath() == $this->getUrl(), 'Redirected from the inactive domain.'); + $this->assertResponse(200, 'Request to trusted host allowed.'); + + // The redirect is cached, so we must flush cache to test again. + drupal_flush_all_caches(); + + // Test another inactive domain that is not trusted. + // Disable the domain and test for redirect. + $domain3->saveDefault(); + $this->drupalGet($domain->getPath()); + $this->assertRaw('The provided host name is not a valid redirect.'); } } diff --git a/domain/tests/src/Functional/DomainListBuilderTest.php b/domain/tests/src/Functional/DomainListBuilderTest.php index 410630d1..05c4437e 100644 --- a/domain/tests/src/Functional/DomainListBuilderTest.php +++ b/domain/tests/src/Functional/DomainListBuilderTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\domain\Functional; -use Drupal\Tests\domain\Functional\DomainTestBase; +use Drupal\domain\DomainInterface; /** * Tests behavior for the domain list builder. @@ -16,7 +16,7 @@ class DomainListBuilderTest extends DomainTestBase { * * @var array */ - public static $modules = array('domain', 'user'); + public static $modules = ['domain', 'user']; /** * {@inheritdoc} @@ -24,46 +24,58 @@ class DomainListBuilderTest extends DomainTestBase { protected function setUp() { parent::setUp(); - // Create 5 domains. - $this->domainCreateTestDomains(5); + // Create 150 domains. + $this->domainCreateTestDomains(150); } /** * Basic test setup. */ public function testDomainListBuilder() { - $admin = $this->drupalCreateUser(array( + $admin = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', 'administer node display', 'administer domains', - )); + ]); $this->drupalLogin($admin); $this->drupalGet('admin/config/domain'); $this->assertSession()->statusCodeEquals(200); // Check that links are printed. - foreach ($this->getDomains() as $domain) { + foreach ($this->getPaginatedDomains() as $domain) { $href = 'admin/config/domain/edit/' . $domain->id(); - $this->assertSession()->linkByHrefExists($href, 0, 'Link found'); + $this->assertSession()->linkByHrefExists($href, 0, 'Link found ' . $href); + $this->assertSession()->assertEscaped($domain->label()); + // Check for pagination. + $this->checkPagination(); + } + + // Go to page 2. + $this->clickLink('Next'); + foreach ($this->getPaginatedDomains(1) as $domain) { + $href = 'admin/config/domain/edit/' . $domain->id(); + $this->assertSession()->linkByHrefExists($href, 0, 'Link found ' . $href); $this->assertSession()->assertEscaped($domain->label()); + // Check for pagination. + $this->checkPagination(); } // Now login as a user with limited rights. - $account = $this->drupalCreateUser(array( + $account = $this->drupalCreateUser([ 'create article content', 'edit any article content', 'edit assigned domains', 'view domain list', - )); + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $account->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); $user_storage = \Drupal::entityTypeManager()->getStorage('user'); $user = $user_storage->load($account->id()); $manager = \Drupal::service('domain.element_manager'); - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 2, 'User saved with two domain records.'); $this->drupalLogin($account); @@ -72,9 +84,9 @@ public function testDomainListBuilder() { $this->assertSession()->statusCodeEquals(200); // Check that links are printed. - $path = 'admin/config/domain/'; + $path = 'admin/config/domain'; $this->drupalGet($path); - foreach ($this->getDomains() as $domain) { + foreach ($this->getPaginatedDomains() as $domain) { $href = 'admin/config/domain/edit/' . $domain->id(); if (in_array($domain->id(), $ids)) { $this->assertSession()->linkByHrefExists($href, 0, 'Link found'); @@ -84,10 +96,12 @@ public function testDomainListBuilder() { $this->assertSession()->linkByHrefNotExists($href, 'Link not found'); $this->assertSession()->assertEscaped($domain->label()); } + // Check for pagination. + $this->checkPagination(); } // Check access to the pages/routes. - foreach ($this->getDomains() as $domain) { + foreach ($this->getPaginatedDomains() as $domain) { $path = 'admin/config/domain/edit/' . $domain->id(); $this->drupalGet($path); if (in_array($domain->id(), $ids)) { @@ -98,19 +112,36 @@ public function testDomainListBuilder() { } } + // Go to page 2. + $this->drupalGet('admin/config/domain'); + $this->clickLink('Next'); + foreach ($this->getPaginatedDomains(1) as $domain) { + $href = 'admin/config/domain/edit/' . $domain->id(); + if (in_array($domain->id(), $ids)) { + $this->assertSession()->linkByHrefExists($href, 0, 'Link found'); + $this->assertSession()->assertEscaped($domain->label()); + } + else { + $this->assertSession()->linkByHrefNotExists($href, 'Link not found'); + $this->assertSession()->assertEscaped($domain->label()); + } + // Check for pagination. + $this->checkPagination(); + } + // Now login as a user with more limited rights. - $account2 = $this->drupalCreateUser(array( + $account2 = $this->drupalCreateUser([ 'create article content', 'edit any article content', 'edit assigned domains', 'view assigned domains', - )); + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account2->id(), $ids, DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $account2->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); $user_storage = \Drupal::entityTypeManager()->getStorage('user'); $user = $user_storage->load($account2->id()); $manager = \Drupal::service('domain.element_manager'); - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 2, 'User saved with two domain records.'); $this->drupalLogin($account2); @@ -119,9 +150,9 @@ public function testDomainListBuilder() { $this->assertSession()->statusCodeEquals(200); // Check that domains are listed and links are printed. - $path = 'admin/config/domain/'; + $path = 'admin/config/domain'; $this->drupalGet($path); - foreach ($this->getDomains() as $domain) { + foreach ($this->getPaginatedDomains() as $domain) { $href = 'admin/config/domain/edit/' . $domain->id(); if (in_array($domain->id(), $ids)) { $this->assertSession()->linkByHrefExists($href, 0, 'Link found'); @@ -131,10 +162,12 @@ public function testDomainListBuilder() { $this->assertSession()->linkByHrefNotExists($href, 'Link not found'); $this->assertSession()->assertNoEscaped($domain->label()); } + // Check for pagination. + $this->checkNoPagination(); } // Check access to the pages/routes. - foreach ($this->getDomains() as $domain) { + foreach ($this->getPaginatedDomains() as $domain) { $path = 'admin/config/domain/edit/' . $domain->id(); $this->drupalGet($path); if (in_array($domain->id(), $ids)) { @@ -144,6 +177,37 @@ public function testDomainListBuilder() { $this->assertSession()->statusCodeEquals(403); } } + + } + + /** + * Returns an array of domains, paginated and sorted by weight. + * + * @param int $page + * The page number to return. + */ + private function getPaginatedDomains($page = 0) { + $limit = 50; + $offset = $page * $limit; + return array_slice($this->getDomainsSorted(), $offset, $limit); + } + + /** + * Checks that pagination links appear, as expected. + */ + private function checkPagination() { + foreach (['?page=0', '?page=1', '?page=2'] as $href) { + $this->assertSession()->linkByHrefExists($href, 0, 'Link found'); + } + } + + /** + * Checks that pagination links do not appear, as expected. + */ + private function checkNoPagination() { + foreach (['?page=0', '?page=1', '?page=2'] as $href) { + $this->assertSession()->linkByHrefNotExists($href, 0, 'Link not found'); + } } } diff --git a/domain/tests/src/Functional/DomainListWeightTest.php b/domain/tests/src/Functional/DomainListWeightTest.php new file mode 100644 index 00000000..52699eab --- /dev/null +++ b/domain/tests/src/Functional/DomainListWeightTest.php @@ -0,0 +1,109 @@ +domainCreateTestDomains(60); + } + + /** + * Basic test setup. + */ + public function testDomainWeight() { + // Test the default sort values. Should be 1 to 60. + $domains = $this->getDomainsSorted(); + $i = 1; + foreach ($domains as $domain) { + $this->assert($domain->getWeight() == $i, 'Weight set to ' . $i); + $i++; + } + // The last domain should be test59_example_com. + $this->assert($domain->id() == 'test59_example_com', 'Last domain is test59'); + $domains_old = $domains; + + $admin = $this->drupalCreateUser([ + 'bypass node access', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer domains', + ]); + $this->drupalLogin($admin); + + $this->drupalGet('admin/config/domain'); + $this->assertSession()->statusCodeEquals(200); + + // Set one weight to 61. + $locator = 'edit-domains-one-example-com-weight'; + $this->fillField($locator, 61); + + // Save the form. + $this->pressButton('edit-submit'); + + $domains = $this->getDomainsSorted(); + $i = 1; + foreach ($domains as $domain) { + // Weights should be the same one page 1 except for the one we changed. + if ($domain->id() == 'one_example_com') { + $this->assert($domain->getWeight() == 61, 'Weight set to 61 ' . $domain->getWeight()); + } + else { + $this->assert($domain->getWeight() == $domains_old[$domain->id()]->getWeight(), 'Weights unchanged'); + } + $i++; + } + // The last domain should be one_example_com. + $this->assert($domain->id() == 'one_example_com', 'Last domain is one'); + + // Go to page two. + $this->clickLink('Next'); + $this->assertSession()->statusCodeEquals(200); + // Set one weight to 2. + $locator = 'edit-domains-one-example-com-weight'; + $this->fillField($locator, 2); + // Save the form. + $this->pressButton('edit-submit'); + + $this->drupalGet('admin/config/domain'); + $this->assertSession()->statusCodeEquals(200); + + // Go to page two. + $this->clickLink('Next'); + $this->assertSession()->statusCodeEquals(200); + + // Check the domain sort order. + $domains = $this->getDomainsSorted(); + $i = 1; + foreach ($domains as $domain) { + if ($domain->id() == 'one_example_com') { + $this->assert($domain->getWeight() == 2, 'Weight set to 2'); + } + else { + $this->assert($domain->getWeight() == $domains_old[$domain->id()]->getWeight(), 'Weights unchanged'); + } + } + // The last domain should be test59_example_com. + $this->assert($domain->id() == 'test59_example_com', 'Last domain is test59' . $domain->id()); + } + +} diff --git a/domain/tests/src/Functional/DomainNavBlockTest.php b/domain/tests/src/Functional/DomainNavBlockTest.php new file mode 100644 index 00000000..ae44c03d --- /dev/null +++ b/domain/tests/src/Functional/DomainNavBlockTest.php @@ -0,0 +1,97 @@ +domainCreateTestDomains(4); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + + // Place the nav block. + $block = $this->drupalPlaceBlock('domain_nav_block'); + + // Let the anon user view the block. + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['use domain nav block']); + + // Load the homepage. All links should appear. + $this->drupalGet(''); + // Confirm domain links. + foreach ($domains as $id => $domain) { + $this->findLink($domain->label()); + } + + // Disable one of the domains. One link should not appear. + $disabled = $domains['one_example_com']; + $disabled->disable(); + + // Load the homepage. + $this->drupalGet(''); + // Confirm domain links. + foreach ($domains as $id => $domain) { + if ($id != 'one_example_com') { + $this->findLink($domain->label()); + } + else { + $this->assertNoRaw($domain->label()); + } + } + // Let the anon user view disabled domains. All links should appear. + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['access inactive domains']); + + // Load the homepage. + $this->drupalGet(''); + // Confirm domain links. + foreach ($domains as $id => $domain) { + $this->findLink($domain->label()); + } + + // Now update the configuration and test again. + $this->config('block.block.' . $block->id()) + ->set('settings.link_options', 'active') + ->set('settings.link_label', 'hostname') + ->save(); + + // Load the the login page. + $this->drupalGet('user/login'); + // Confirm domain links. + foreach ($domains as $id => $domain) { + $this->findLink($domain->getHostname()); + $this->assertRaw($domain->buildUrl(base_path() . 'user/login')); + } + + // Now update the configuration and test again. + $this->config('block.block.' . $block->id()) + ->set('settings.link_options', 'home') + ->set('settings.link_theme', 'menu') + ->set('settings.link_label', 'url') + ->save(); + + // Load the the login page. + $this->drupalGet('user/login'); + // Confirm domain links. + foreach ($domains as $id => $domain) { + $this->findLink($domain->getPath()); + $this->assertRaw($domain->getPath()); + } + } + +} diff --git a/domain/src/Tests/DomainNegotiatorTest.php b/domain/tests/src/Functional/DomainNegotiatorTest.php similarity index 80% rename from domain/src/Tests/DomainNegotiatorTest.php rename to domain/tests/src/Functional/DomainNegotiatorTest.php index 8ac4e629..01003096 100644 --- a/domain/src/Tests/DomainNegotiatorTest.php +++ b/domain/tests/src/Functional/DomainNegotiatorTest.php @@ -1,6 +1,6 @@ drupalPlaceBlock('domain_server_block'); // To get around block access, let the anon user view the block. - user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('view domain information')); + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['view domain information']); // Test the response of the default home page. - foreach (\Drupal::service('domain.loader')->loadMultiple() as $domain) { + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultiple() as $domain) { $this->drupalGet($domain->getPath()); $this->assertRaw($domain->label(), 'Loaded the proper domain.'); } diff --git a/domain/tests/src/Functional/DomainPageCacheTest.php b/domain/tests/src/Functional/DomainPageCacheTest.php new file mode 100644 index 00000000..124e0e5e --- /dev/null +++ b/domain/tests/src/Functional/DomainPageCacheTest.php @@ -0,0 +1,38 @@ +domainTableIsEmpty(); + + // Create a new domain programmatically. + $this->domainCreateTestDomains(5); + $expected = []; + + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath()); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . ':'; + } + + $database = \Drupal::database(); + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), 'Cache returns as expected.'); + + } + +} diff --git a/domain/tests/src/Functional/DomainReferencesTest.php b/domain/tests/src/Functional/DomainReferencesTest.php index b3b3acc7..9eb424df 100644 --- a/domain/tests/src/Functional/DomainReferencesTest.php +++ b/domain/tests/src/Functional/DomainReferencesTest.php @@ -2,13 +2,14 @@ namespace Drupal\Tests\domain\Functional; -use Drupal\Tests\domain\Functional\DomainTestBase; +use Drupal\domain\DomainInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Tests behavior for hook_domain_references_alter(). * - * The module suite ships with two field types -- admin and editor. We want to ensure - * that these are filtered properly by hook_domain_references_alter(). + * The module suite ships with two field types -- admin and editor. We want to + * ensure that these are filtered properly by hook_domain_references_alter(). * * @group domain */ @@ -19,7 +20,13 @@ class DomainReferencesTest extends DomainTestBase { * * @var array */ - public static $modules = array('domain', 'domain_access', 'field', 'field_ui', 'user'); + public static $modules = [ + 'domain', + 'domain_access', + 'field', + 'field_ui', + 'user', + ]; /** * {@inheritdoc} @@ -35,70 +42,84 @@ protected function setUp() { * Basic test setup. */ public function testDomainReferences() { - $admin = $this->drupalCreateUser(array( + // Create an admin user. This will be user 2. + $admin = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer users', 'administer domains', - )); + 'assign domain editors', + ]); $this->drupalLogin($admin); $this->drupalGet('admin/people/create'); $this->assertSession()->statusCodeEquals(200); - // Create a user through the form. + // Create a user through the form. This will be user 3. $this->fillField('name', 'testuser'); $this->fillField('mail', 'test@example.com'); $this->fillField('pass[pass1]', 'test'); $this->fillField('pass[pass2]', 'test'); - // We expect to find 5 domain options. We set two as selected. - $domains = \Drupal::service('domain.loader')->loadMultiple(); - $count = 0; + // We expect to find 5 domain options. We set three as selected. + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $ids = ['example_com', 'one_example_com', 'two_example_com']; + $edit_ids = ['example_com', 'one_example_com']; foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if (in_array($domain->id(), $ids)) { $this->checkField($locator); } - $locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); + if (in_array($domain->id(), $edit_ids)) { + $this->checkField($locator); + } } // Find the all affiliates field. - $locator = DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; $this->findField($locator); // Save the form. $this->pressButton('edit-submit'); $this->assertSession()->statusCodeEquals(200); + // Load our test user. $storage = \Drupal::entityTypeManager()->getStorage('user'); - $user = $storage->load(3); - // Check that two values are set. + $testuser = $storage->load(3); + // Check that three values are set. $manager = \Drupal::service('domain.element_manager'); - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); - $this->assert(count($values) == 3, 'User saved with three domain records.'); - - // Now login as a user with limited rights. - $account = $this->drupalCreateUser(array( + $values = $manager->getFieldValues($testuser, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->assert(count($values) == 3, 'User saved with three domain admin records.'); + // Check that no access fields are set. + $values = $manager->getFieldValues($testuser, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain access records.'); + + // Now login as a user with limited rights. This is user 4. + $account = $this->drupalCreateUser([ 'administer users', 'assign domain administrators', - )); + ]); + // Set some domain assignments for this user. $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD); - $tester = $storage->load($account->id()); - $values = $manager->getFieldValues($tester, DOMAIN_ADMIN_FIELD); - $this->assert(count($values) == 2, 'User saved with two domain records.'); - $storage->resetCache(array($account->id())); + $this->addDomainsToEntity('user', $account->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); + $limited_admin = $storage->load($account->id()); + $values = $manager->getFieldValues($limited_admin, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain admin records.'); + // Check that no access fields are set. + $values = $manager->getFieldValues($limited_admin, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->assert(count($values) == 0, 'User saved with no domain access records.'); + + // Now edit user 3 as user 4 with limited rights. $this->drupalLogin($account); - - $this->drupalGet('user/' . $user->id() . '/edit'); + $this->drupalGet('user/' . $testuser->id() . '/edit'); $this->assertSession()->statusCodeEquals(200); foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if ($domain->id() == 'example_com') { $this->checkField($locator); @@ -110,12 +131,12 @@ public function testDomainReferences() { $this->assertSession()->fieldNotExists($locator); } // No Domain Access field rights exist for this user. - $locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; $this->assertSession()->fieldNotExists($locator); } // The all affiliates field should not be present.. - $locator = DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; $this->assertSession()->fieldNotExists($locator); // Save the form. @@ -123,36 +144,41 @@ public function testDomainReferences() { $this->assertSession()->statusCodeEquals(200); // Now, check the user. - $storage->resetCache(array($user->id())); - $user = $storage->load($user->id()); + $storage->resetCache([$testuser->id()]); + $testuser = $storage->load($testuser->id()); // Check that two values are set. - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); - $this->assert(count($values) == 2, 'User saved with two domain records.'); - - // Now login as a user with different limited rights. - $account = $this->drupalCreateUser(array( + $values = $manager->getFieldValues($testuser, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain admin records.'); + // Check that no access fields are set. + $values = $manager->getFieldValues($testuser, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain access records.'); + + // Now login as a user with different limited rights. This is user 5. + $new_account = $this->drupalCreateUser([ 'administer users', 'assign domain administrators', 'assign domain editors', - )); + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD); $new_ids = ['one_example_com', 'four_example_com']; - $this->addDomainsToEntity('user', $account->id(), $new_ids, DOMAIN_ACCESS_FIELD); - - $tester = $storage->load($account->id()); - $values = $manager->getFieldValues($tester, DOMAIN_ADMIN_FIELD); - $this->assert(count($values) == 2, 'User saved with two domain records.'); - $values = $manager->getFieldValues($tester, DOMAIN_ACCESS_FIELD); - $this->assert(count($values) == 2, 'User saved with two domain records.'); - $storage->resetCache(array($account->id())); - $this->drupalLogin($account); + $this->addDomainsToEntity('user', $new_account->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $new_account->id(), $new_ids, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + $new_admin = $storage->load($new_account->id()); + $values = $manager->getFieldValues($new_admin, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain admin records.'); + $values = $manager->getFieldValues($new_admin, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain access records.'); + + // Now edit the user as someone with limited rights. + $storage->resetCache([$new_admin->id()]); + $this->drupalLogin($new_account); - $this->drupalGet('user/' . $user->id() . '/edit'); + $this->drupalGet('user/' . $testuser->id() . '/edit'); $this->assertSession()->statusCodeEquals(200); foreach ($domains as $domain) { - $locator = DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; + $locator = DomainInterface::DOMAIN_ADMIN_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if ($domain->id() == 'example_com') { $this->checkField($locator); @@ -163,8 +189,9 @@ public function testDomainReferences() { else { $this->assertSession()->fieldNotExists($locator); } - // Some Domain Access field rights exist for this user. This adds one to the count. - $locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + // Some Domain Access field rights exist for this user. This adds + // one to the count. + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; if (in_array($domain->id(), $new_ids)) { $this->findField($locator); $this->checkField($locator); @@ -175,7 +202,7 @@ public function testDomainReferences() { } // The all affiliates field should not be present.. - $locator = DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; $this->assertSession()->fieldNotExists($locator); // Save the form. @@ -183,13 +210,13 @@ public function testDomainReferences() { $this->assertSession()->statusCodeEquals(200); // Now, check the user. - $storage->resetCache(array($user->id())); - $user = $storage->load($user->id()); + $storage->resetCache([$testuser->id()]); + $testuser = $storage->load($testuser->id()); // Check that two values are set. - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); - $this->assert(count($values) == 2, 'User saved with two domain records.'); - $values = $manager->getFieldValues($user, DOMAIN_ACCESS_FIELD); - $this->assert(count($values) == 3, 'User saved with three domain records.'); + $values = $manager->getFieldValues($testuser, DomainInterface::DOMAIN_ADMIN_FIELD); + $this->assert(count($values) == 2, 'User saved with two domain admin records.'); + $values = $manager->getFieldValues($testuser, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->assert(count($values) == 3, 'User saved with three domain access records.'); } diff --git a/domain/tests/src/Functional/DomainTestBase.php b/domain/tests/src/Functional/DomainTestBase.php index eea6f8aa..93f85a05 100644 --- a/domain/tests/src/Functional/DomainTestBase.php +++ b/domain/tests/src/Functional/DomainTestBase.php @@ -2,28 +2,47 @@ namespace Drupal\Tests\domain\Functional; -use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Session\AccountInterface; use Drupal\Component\Utility\Crypt; use Drupal\Tests\BrowserTestBase; -use Drupal\user\UserInterface; use Drupal\domain\DomainInterface; +use Drupal\Tests\domain\Traits\DomainTestTrait; +/** + * Class DomainTestBase. + * + * @package Drupal\Tests\domain\Functional + */ abstract class DomainTestBase extends BrowserTestBase { + use DomainTestTrait; + /** * Sets a base hostname for running tests. * - * When creating test domains, try to use $this->base_hostname or the + * When creating test domains, try to use $this->baseHostname or the * domainCreateTestDomains() method. + * + * @var string */ - public $base_hostname; + public $baseHostname; + + /** + * Sets a base TLD for running tests. + * + * When creating test domains, try to use $this->baseTLD or the + * domainCreateTestDomains() method. + * + * @var string + */ + public $baseTLD; /** * Modules to enable. * * @var array */ - public static $modules = array('domain', 'node'); + public static $modules = ['domain', 'node']; /** * We use the standard profile for testing. @@ -32,6 +51,13 @@ abstract class DomainTestBase extends BrowserTestBase { */ protected $profile = 'standard'; + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + /** * {@inheritdoc} */ @@ -39,92 +65,18 @@ protected function setUp() { parent::setUp(); // Set the base hostname for domains. - $this->base_hostname = \Drupal::service('domain.creator')->createHostname(); - } + $this->baseHostname = \Drupal::entityTypeManager()->getStorage('domain')->createHostname(); - /** - * Generates a list of domains for testing. - * - * In my environment, I use the example.com hostname as a base. Then I name - * hostnames one.* two.* up to ten. Note that we always use *_example_com - * for the machine_name (entity id) value, though the hostname can vary - * based on the system. This naming allows us to load test schema files. - * - * The script may also add test1, test2, test3 up to any number to test a - * large number of domains. - * - * @param int $count - * The number of domains to create. - * @param string|NULL $base_hostname - * The root domain to use for domain creation (e.g. example.com). - * @param array $list - * An optional list of subdomains to apply instead of the default set. - */ - public function domainCreateTestDomains($count = 1, $base_hostname = NULL, $list = array()) { - $original_domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - if (empty($base_hostname)) { - $base_hostname = $this->base_hostname; - } - // Note: these domains are rigged to work on my test server. - // For proper testing, yours should be set up similarly, but you can pass a - // $list array to change the default. - if (empty($list)) { - $list = array('', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'); - } - for ($i = 0; $i < $count; $i++) { - if (!empty($list[$i])) { - if ($i < 11) { - $hostname = $list[$i] . '.' . $base_hostname; - $machine_name = $list[$i] . '.example.com'; - $name = ucfirst($list[$i]); - } - // These domains are not setup and are just for UX testing. - else { - $hostname = 'test' . $i . '.' . $base_hostname; - $machine_name = 'test' . $i . '.example.com'; - $name = 'Test ' . $i; - } - } - else { - $hostname = $base_hostname; - $machine_name = 'example.com'; - $name = 'Example'; - } - // Create a new domain programmatically. - $values = array( - 'hostname' => $hostname, - 'name' => $name, - 'id' => \Drupal::service('domain.creator')->createMachineName($machine_name), - ); - $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); - $domain->save(); - } - $domains = \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); - $this->assertTrue((count($domains) - count($original_domains)) == $count, new FormattableMarkup('Created %count new domains.', array('%count' => $count))); - } + // Ensure that $this->baseTLD is set. + $this->setBaseTLD(); - /** - * Adds a test domain to an entity. - * - * @param string $entity_type - * The entity type being acted upon. - * @param int $entity_id - * The entity id. - * @param array $ids - * An array of ids to add. - * @param string $field - * The name of the domain field used to attach to the entity. - */ - public function addDomainsToEntity($entity_type, $entity_id, $ids, $field) { - if ($entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) { - $entity->set($field, $ids); - $entity->save(); - } + $this->database = $this->getDatabaseConnection(); } /** - * The methods below are brazenly copied from Rules module. They are all - * helper methods that make writing tests a bit easier. + * The methods below are brazenly copied from Rules module. + * + * They are all helper methods that make writing tests a bit easier. */ /** @@ -140,6 +92,19 @@ public function findLink($locator) { return $this->getSession()->getPage()->findLink($locator); } + /** + * Confirms absence of link with specified locator. + * + * @param string $locator + * Link id, title, text or image alt. + * + * @return bool + * TRUE if link is absent, or FALSE. + */ + public function findNoLink($locator) { + return empty($this->getSession()->getPage()->hasLink($locator)); + } + /** * Finds field (input, textarea, select) with specified locator. * @@ -153,6 +118,19 @@ public function findField($locator) { return $this->getSession()->getPage()->findField($locator); } + /** + * Finds no field exists (input, textarea, select) with specified locator. + * + * @param string $locator + * Input id, name or label. + * + * @return \Behat\Mink\Element\NodeElement|null + * The input field element. + */ + public function findNoField($locator) { + return $this->assertSession()->fieldNotExists($locator);; + } + /** * Finds button with specified locator. * @@ -197,9 +175,10 @@ public function fillField($locator, $value) { /** * Checks checkbox with specified locator. * - * @param string $locator input id, name or label + * @param string $locator + * An input id, name or label. * - * @throws ElementNotFoundException + * @throws \Behat\Mink\Exception\ElementNotFoundException */ public function checkField($locator) { $this->getSession()->getPage()->checkField($locator); @@ -208,9 +187,10 @@ public function checkField($locator) { /** * Unchecks checkbox with specified locator. * - * @param string $locator input id, name or label + * @param string $locator + * An input id, name or label. * - * @throws ElementNotFoundException + * @throws \Behat\Mink\Exception\ElementNotFoundException */ public function uncheckField($locator) { $this->getSession()->getPage()->uncheckField($locator); @@ -219,25 +199,69 @@ public function uncheckField($locator) { /** * Selects option from select field with specified locator. * - * @param string $locator input id, name or label - * @param string $value option value - * @param Boolean $multiple select multiple options + * @param string $locator + * An input id, name or label. + * @param string $value + * The option value. + * @param bool $multiple + * Whether to select multiple options. * - * @throws ElementNotFoundException + * @throws \Behat\Mink\Exception\ElementNotFoundException * * @see NodeElement::selectOption */ - public function selectFieldOption($locator, $value, $multiple = false) { + public function selectFieldOption($locator, $value, $multiple = FALSE) { $this->getSession()->getPage()->selectFieldOption($locator, $value, $multiple); } /** - * Returns an uncached list of all domains. + * Returns whether a given user account is logged in. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The user account object to check. + * + * @return bool + * TRUE if a given user account is logged in, or FALSE. + */ + protected function drupalUserIsLoggedIn(AccountInterface $account) { + // @TODO: This is a temporary hack for the test login fails when setting $cookie_domain. + if (!isset($account->session_id)) { + return (bool) $account->id(); + } + // The session ID is hashed before being stored in the database. + // @see \Drupal\Core\Session\SessionHandler::read() + return (bool) $this->database->query("SELECT sid FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", [':sid' => Crypt::hashBase64($account->session_id)])->fetchField(); + } + + /** + * Login a user on a specific domain. * - * @return array - * An array of domain entities. + * @param \Drupal\domain\DomainInterface $domain + * The domain to log the user into. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account to login. */ - public function getDomains() { - return \Drupal::service('domain.loader')->loadMultiple(NULL, TRUE); + public function domainLogin(DomainInterface $domain, AccountInterface $account) { + // Due to a quirk in session handling that we cannot directly access, it + // works if we login, then logout, and then login to a specific domain. + $this->drupalLogin($account); + if ($this->loggedInUser) { + $this->drupalLogout(); + } + + // Login. + $url = $domain->getPath() . 'user/login'; + $this->submitForm([ + 'name' => $account->getAccountName(), + 'pass' => $account->passRaw, + ], t('Log in')); + + // @see BrowserTestBase::drupalUserIsLoggedIn() + $account->sessionId = $this->getSession()->getCookie($this->getSessionName()); + $this->assertTrue($this->drupalUserIsLoggedIn($account), 'User successfully logged in.'); + + $this->loggedInUser = $account; + $this->container->get('current_user')->setAccount($account); } + } diff --git a/domain/src/Tests/DomainTokenTest.php b/domain/tests/src/Functional/DomainTokenTest.php similarity index 89% rename from domain/src/Tests/DomainTokenTest.php rename to domain/tests/src/Functional/DomainTokenTest.php index fabc738d..5f5039e1 100644 --- a/domain/src/Tests/DomainTokenTest.php +++ b/domain/tests/src/Functional/DomainTokenTest.php @@ -1,9 +1,8 @@ drupalPlaceBlock('domain_token_block'); // To get around block access, let the anon user view the block. - user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, array('view domain information')); + user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, ['view domain information']); // Test the response of the default home page. - foreach (\Drupal::service('domain.loader')->loadMultiple() as $domain) { + foreach (\Drupal::entityTypeManager()->getStorage('domain')->loadMultiple() as $domain) { $this->drupalGet($domain->getPath()); $this->assertRaw($domain->label(), 'Loaded the proper domain.'); $this->assertRaw('Token', 'Token values printed.'); diff --git a/domain/tests/src/Functional/DomainValidatorTest.php b/domain/tests/src/Functional/DomainValidatorTest.php new file mode 100644 index 00000000..6f21119a --- /dev/null +++ b/domain/tests/src/Functional/DomainValidatorTest.php @@ -0,0 +1,99 @@ +domainTableIsEmpty(); + $validator = \Drupal::service('domain.validator'); + $storage = \Drupal::entityTypeManager()->getStorage('domain'); + + // Create a domain. + $this->domainCreateTestDomains(1, 'foo.com'); + // Check the created domain based on its known id value. + $key = 'foo.com'; + /** @var \Drupal\domain\Entity\Domain $domain */ + $domain = $storage->loadByHostname($key); + $this->assertNotEmpty($domain, 'Test domain created.'); + + // Valid hostnames to test. Valid is the boolean value. + $hostnames = [ + 'localhost' => 1, + 'example.com' => 1, + // See www-prefix test, below. + 'www.example.com' => 1, + 'one.example.com' => 1, + 'example.com:8080' => 1, + // Only one colon. + 'example.com::8080' => 0, + // No letters after a colon. + 'example.com:abc' => 0, + // Cannot begin with a dot. + '.example.com' => 0, + // Cannot end with a dot. + 'example.com.' => 0, + // Lowercase only. + 'EXAMPLE.com' => 0, + // ascii-only. + 'éxample.com' => 0, + ]; + foreach ($hostnames as $hostname => $valid) { + $errors = $validator->validate($hostname); + if ($valid) { + $this->assertEmpty($errors, 'Validation correct with no errors.'); + } + else { + $this->assertNotEmpty($errors, 'Validation correct with proper errors.'); + } + } + // Test duplicate hostname creation. + $test_hostname = 'foo.com'; + $test_domain = $storage->create([ + 'hostname' => $test_hostname, + 'name' => 'Test domain', + 'id' => 'test_domain', + ]); + try { + $test_domain->save(); + $this->fail('Duplicate hostname validation'); + } + catch (ConfigValueException $e) { + $expected_message = "The hostname ($test_hostname) is already registered."; + $this->assertEqual($expected_message, $e->getMessage()); + } + // Test the two configurable options. + $config = $this->config('domain.settings'); + $config->set('www_prefix', TRUE); + $config->set('allow_non_ascii', TRUE); + $config->save(); + // Valid hostnames to test. Valid is the boolean value. + $hostnames = [ + // No www-prefix allowed. + 'www.example.com' => 0, + // ascii-only allowed. + 'éxample.com' => 1, + ]; + foreach ($hostnames as $hostname => $valid) { + $errors = $validator->validate($hostname); + if ($valid) { + $this->assertEmpty($errors, 'Validation test correct with no errors.'); + } + else { + $this->assertNotEmpty($errors, 'Validation test correct with errors.'); + } + } + } + +} diff --git a/domain/src/Tests/DomainViewsAccessTest.php b/domain/tests/src/Functional/DomainViewsAccessTest.php similarity index 71% rename from domain/src/Tests/DomainViewsAccessTest.php rename to domain/tests/src/Functional/DomainViewsAccessTest.php index 2f8387a4..c028d295 100644 --- a/domain/src/Tests/DomainViewsAccessTest.php +++ b/domain/tests/src/Functional/DomainViewsAccessTest.php @@ -1,6 +1,6 @@ domainCreateTestDomains(5); - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); // Enable the views. $this->enableViewsTestModule(); // Create a user. To test the area output was more difficult, so we just // configured two views. The page shows the first, admin, user, and the // block will show this new user name. - $this->user = $this->drupalCreateUser(array('administer domains', 'create domains')); + $this->user = $this->drupalCreateUser(['administer domains', 'create domains']); // Place the view block. $this->drupalPlaceBlock('views_block:domain_views_access-block_1'); @@ -48,20 +41,20 @@ public function testInactiveDomain() { if (in_array($domain->id(), $allowed)) { $this->assertResponse('200', 'Access allowed'); $this->assertRaw('admin'); - $this->assertRaw($this->user->getUsername()); + $this->assertRaw($this->user->getAccountName()); } else { $this->assertResponse('403', 'Access denied'); $this->assertNoRaw('admin'); - $this->assertNoRaw($this->user->getUsername()); + $this->assertNoRaw($this->user->getAccountName()); } // Test the block on another page. $this->drupalGet($domain->getPath()); if (in_array($domain->id(), $allowed)) { - $this->assertRaw($this->user->getUsername()); + $this->assertRaw($this->user->getAccountName()); } else { - $this->assertNoRaw($this->user->getUsername()); + $this->assertNoRaw($this->user->getAccountName()); } } } @@ -73,7 +66,7 @@ public function testInactiveDomain() { * using it, it cannot be enabled normally. */ protected function enableViewsTestModule() { - \Drupal::service('module_installer')->install(array('domain_test')); + \Drupal::service('module_installer')->install(['domain_test']); $this->resetAll(); $this->rebuildContainer(); $this->container->get('module_handler')->reload(); diff --git a/domain/tests/src/Functional/Views/ActiveDomainDefaultArgumentTest.php b/domain/tests/src/Functional/Views/ActiveDomainDefaultArgumentTest.php new file mode 100644 index 00000000..baa71efa --- /dev/null +++ b/domain/tests/src/Functional/Views/ActiveDomainDefaultArgumentTest.php @@ -0,0 +1,96 @@ +domainCreateTestDomains(3); + $this->createTestData(); + } + + /** + * {@inheritdoc} + */ + protected function createTestData() { + foreach ($this->getDomains() as $domain_id => $domain) { + $nodes_count = random_int(1, 5); + while ($nodes_count !== 0) { + $node = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => $this->randomString(), + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => $domain_id, + ]); + $this->data[$domain_id][] = $node->id(); + $nodes_count--; + } + } + } + + /** + * Tests active_domain default argument. + */ + public function testActiveDomainDefaultArgument() { + $url = Url::fromRoute('view.test_active_domain_argument.page_1'); + + foreach ($this->getDomains() as $domain_id => $domain) { + $page_url = $domain->buildUrl($url->toString()); + $this->drupalGet($page_url); + + $expected_nids = array_values($this->data[$domain_id]); + $this->assertNids($domain_id, $expected_nids); + } + } + + /** + * Ensures that a list of nodes appear on the page. + * + * @param string $domain_id + * Domain ID. + * @param array $expected_nids + * An array of node IDs. + */ + protected function assertNids($domain_id, array $expected_nids = []) { + $result = $this->xpath("//td[contains(@class, 'views-field-nid')]"); + $actual_nids = []; + foreach ($result as $element) { + $actual_nids[] = $element->getText(); + } + + $this->assertSame($expected_nids, $actual_nids, 'Domain ID: ' . $domain_id); + } + +} diff --git a/domain/tests/src/Kernel/DomainConfigTest.php b/domain/tests/src/Kernel/DomainConfigTest.php new file mode 100644 index 00000000..79508aef --- /dev/null +++ b/domain/tests/src/Kernel/DomainConfigTest.php @@ -0,0 +1,39 @@ +installEntitySchema('domain'); + // Install the test domain record & views config. + $this->installConfig('domain_config_schema_test'); + } + + /** + * Dummy test method to ensure config gets installed. + */ + public function testRun() { + $this->assertTrue(TRUE); + } + +} diff --git a/domain/tests/src/Kernel/DomainHookTest.php b/domain/tests/src/Kernel/DomainHookTest.php new file mode 100644 index 00000000..3d661e76 --- /dev/null +++ b/domain/tests/src/Kernel/DomainHookTest.php @@ -0,0 +1,178 @@ +baseHostname or the + * domainCreateTestDomains() method. + * + * @var string + */ + public $baseHostname; + + /** + * Test setup. + */ + protected function setUp() { + parent::setUp(); + + // Create a domain. + $this->domainCreateTestDomains(); + + // Get the services. + $this->domainStorage = \Drupal::entityTypeManager()->getStorage('domain'); + $this->currentUser = \Drupal::service('current_user'); + $this->moduleHandler = \Drupal::service('module_handler'); + } + + /** + * Tests domain loading. + */ + public function testHookDomainLoad() { + // Check the created domain based on its known id value. + $domain = $this->domainStorage->load($this->key); + + // Internal hooks. + $path = $domain->getPath(); + $url = $domain->getUrl(); + $this->assertTrue(isset($path), new FormattableMarkup('The path property was set to %path by hook_entity_load.', ['%path' => $path])); + $this->assertTrue(isset($url), new FormattableMarkup('The url property was set to %url by hook_entity_load.', ['%url' => $url])); + + // External hooks. + $this->assertTrue($domain->foo == 'bar', 'The foo property was set to bar by hook_domain_load.'); + } + + /** + * Tests domain validation. + */ + public function testHookDomainValidate() { + $validator = \Drupal::service('domain.validator'); + // Test a good domain. + $errors = $validator->validate('one.example.com'); + $this->assertEmpty($errors, 'No errors returned for example.com'); + + // Test our hook implementation, which denies fail.example.com explicitly. + $errors = $validator->validate('fail.example.com'); + $this->assertNotEmpty($errors, 'Errors returned for fail.example.com'); + $this->assertTrue(current($errors) == 'Fail.example.com cannot be registered', 'Error message returned correctly.'); + } + + /** + * Tests domain request alteration. + */ + public function testHookDomainRequestAlter() { + // Set the request. + $negotiator = \Drupal::service('domain.negotiator'); + $negotiator->setRequestDomain($this->baseHostname); + + // Check that the property was added by our hook. + $domain = $negotiator->getActiveDomain(); + $this->assertTrue($domain->foo1 == 'bar1', 'The foo1 property was set to bar1 by hook_domain_request_alter'); + } + + /** + * Tests domain operations hook. + */ + public function testHookDomainOperations() { + $domain = $this->domainStorage->load($this->key); + + // Set the request. + $operations = $this->moduleHandler->invokeAll('domain_operations', [$domain, $this->currentUser]); + + // Test that our operations were added by the hook. + $this->assertArrayHasKey('domain_test', $operations, 'Domain test operation loaded.'); + } + + /** + * Tests domain references alter hook. + */ + public function testHookDomainReferencesAlter() { + $domain = $this->domainStorage->load($this->key); + + // Set the request. + $manager = \Drupal::service('entity_type.manager'); + $target_type = 'domain'; + + // Build a node entity selection query. + $query = $manager->getStorage($target_type)->getQuery(); + $context = [ + 'entity_type' => 'node', + 'bundle' => 'article', + 'field_type' => 'editor', + ]; + + // Run the alteration, which should add metadata to the query for nodes. + $this->moduleHandler->alter('domain_references', $query, $this->currentUser, $context); + $this->assertTrue($query->getMetaData('domain_test') == 'Test string', 'Domain test query altered.'); + + // Build a user entity selection query. + $query = $manager->getStorage($target_type)->getQuery(); + $context = [ + 'entity_type' => 'user', + 'bundle' => 'user', + 'field_type' => 'admin', + ]; + + // Run the alteration, which does not add metadata for user queries. + $this->moduleHandler->alter('domain_references', $query, $this->currentUser, $context); + $this->assertEmpty($query->getMetaData('domain_test'), 'Domain test query not altered.'); + } + +} diff --git a/domain/tests/src/Kernel/DomainVariableSchemeTest.php b/domain/tests/src/Kernel/DomainVariableSchemeTest.php new file mode 100644 index 00000000..8ac3f49e --- /dev/null +++ b/domain/tests/src/Kernel/DomainVariableSchemeTest.php @@ -0,0 +1,86 @@ +baseHostname or the + * domainCreateTestDomains() method. + * + * @var string + */ + public $baseHostname; + + /** + * Test setup. + */ + protected function setUp() { + parent::setUp(); + + // Create a domain. + $this->domainCreateTestDomains(); + + // Get the services. + $this->domainStorage = \Drupal::entityTypeManager()->getStorage('domain'); + } + + /** + * Tests domain loading. + */ + public function testDomainScheme() { + // Set our testing parameters. + $default_scheme = \Drupal::request()->getScheme(); + $alt_scheme = ($default_scheme == 'https') ? 'http' : 'https'; + $add_suffix = FALSE; + + // Our created domain should have a scheme that matches the request. + $domain = $this->domainStorage->load($this->key); + $this->assertTrue($domain->getScheme($add_suffix) == $default_scheme); + + // Swtich the scheme and see if that works. + $domain->set('scheme', $alt_scheme); + $domain->save(); + $domain = $this->domainStorage->load($this->key); + $this->assertTrue($domain->getScheme($add_suffix) == $alt_scheme); + + // Set the scheme to variable, and that should match the default. + $domain->set('scheme', 'variable'); + $domain->save(); + $this->assertTrue($domain->getScheme($add_suffix) == $default_scheme); + } + +} diff --git a/domain/tests/src/Traits/DomainTestTrait.php b/domain/tests/src/Traits/DomainTestTrait.php new file mode 100644 index 00000000..d60a0050 --- /dev/null +++ b/domain/tests/src/Traits/DomainTestTrait.php @@ -0,0 +1,179 @@ +getStorage('domain')->loadMultiple(NULL, TRUE); + if (empty($base_hostname)) { + $base_hostname = $this->baseHostname; + } + // Note: these domains are rigged to work on my test server. + // For proper testing, yours should be set up similarly, but you can pass a + // $list array to change the default. + if (empty($list)) { + $list = [ + '', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'ten', + ]; + } + for ($i = 0; $i < $count; $i++) { + if ($i === 0) { + $hostname = $base_hostname; + $machine_name = 'example.com'; + $name = 'Example'; + } + elseif (!empty($list[$i])) { + $hostname = $list[$i] . '.' . $base_hostname; + $machine_name = $list[$i] . '.example.com'; + $name = 'Test ' . ucfirst($list[$i]); + } + // These domains are not setup and are just for UX testing. + else { + $hostname = 'test' . $i . '.' . $base_hostname; + $machine_name = 'test' . $i . '.example.com'; + $name = 'Test ' . $i; + } + // Create a new domain programmatically. + $values = [ + 'hostname' => $hostname, + 'name' => $name, + 'id' => \Drupal::entityTypeManager()->getStorage('domain')->createMachineName($machine_name), + ]; + $domain = \Drupal::entityTypeManager()->getStorage('domain')->create($values); + $domain->save(); + } + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + } + + /** + * Adds a test domain to an entity. + * + * @param string $entity_type + * The entity type being acted upon. + * @param int $entity_id + * The entity id. + * @param array|string $ids + * An id or array of ids to add. + * @param string $field + * The name of the domain field used to attach to the entity. + */ + public function addDomainsToEntity($entity_type, $entity_id, $ids, $field) { + if ($entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id)) { + $entity->set($field, $ids); + $entity->save(); + } + } + + /** + * Returns an uncached list of all domains. + * + * @return array + * An array of domain entities. + */ + public function getDomains() { + \Drupal::entityTypeManager()->getStorage('domain')->resetCache(); + return \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + } + + /** + * Returns an uncached list of all domains, sorted by weight. + * + * @return array + * An array of domain entities. + */ + public function getDomainsSorted() { + \Drupal::entityTypeManager()->getStorage('domain')->resetCache(); + return \Drupal::entityTypeManager()->getStorage('domain')->loadMultipleSorted(); + } + + /** + * Converts a domain hostname to a trusted host pattern. + * + * @param string $hostname + * A hostname string. + * + * @return string + * A regex-safe hostname, without delimiters. + */ + public function prepareTrustedHostname($hostname) { + $hostname = mb_strtolower(preg_replace('/:\d+$/', '', trim($hostname))); + return preg_quote($hostname); + } + + /** + * Set the base hostname for this test. + */ + public function setBaseHostname() { + $this->baseHostname = \Drupal::entityTypeManager()->getStorage('domain')->createHostname(); + $this->setBaseTLD(); + } + + /** + * Set the base TLD for this test. + */ + public function setBaseTLD() { + $hostname = $this->baseHostname; + $parts = explode('.', $hostname); + $this->baseTLD = array_pop($parts); + } + + /** + * Reusable test function for checking initial / empty table status. + */ + public function domainTableIsEmpty() { + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + $this->assertEmpty($domains, 'No domains have been created.'); + $default_id = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultId(); + $this->assertEmpty($default_id, 'No default domain has been set.'); + } + + /** + * Creates domain record for use with POST request tests. + */ + public function domainPostValues() { + $edit = []; + $domain = \Drupal::entityTypeManager()->getStorage('domain')->create(); + $required = \Drupal::service('domain.validator')->getRequiredFields(); + foreach ($required as $key) { + $edit[$key] = $domain->get($key); + } + // URL validation has issues on Travis, so only do it when requested. + $edit['validate_url'] = 0; + $edit['id'] = \Drupal::entityTypeManager()->getStorage('domain')->createMachineName($edit['hostname']); + return $edit; + } + +} diff --git a/domain_access/config/install/field.storage.node.field_domain_access.yml b/domain_access/config/install/field.storage.node.field_domain_access.yml index a0849e19..d4cdf636 100644 --- a/domain_access/config/install/field.storage.node.field_domain_access.yml +++ b/domain_access/config/install/field.storage.node.field_domain_access.yml @@ -4,6 +4,9 @@ dependencies: module: - domain - node + enforced: + module: + - domain_access id: node.field_domain_access field_name: field_domain_access entity_type: node diff --git a/domain_access/config/install/field.storage.node.field_domain_all_affiliates.yml b/domain_access/config/install/field.storage.node.field_domain_all_affiliates.yml index 59e82379..392f6894 100644 --- a/domain_access/config/install/field.storage.node.field_domain_all_affiliates.yml +++ b/domain_access/config/install/field.storage.node.field_domain_all_affiliates.yml @@ -4,6 +4,9 @@ dependencies: module: - domain - node + enforced: + module: + - domain_access id: node.field_domain_all_affiliates field_name: field_domain_all_affiliates entity_type: node diff --git a/domain_access/config/install/field.storage.user.field_domain_access.yml b/domain_access/config/install/field.storage.user.field_domain_access.yml index 89f4c77f..25d0b094 100644 --- a/domain_access/config/install/field.storage.user.field_domain_access.yml +++ b/domain_access/config/install/field.storage.user.field_domain_access.yml @@ -4,6 +4,9 @@ dependencies: module: - domain - user + enforced: + module: + - domain_access id: user.field_domain_access field_name: field_domain_access entity_type: user diff --git a/domain_access/config/install/field.storage.user.field_domain_all_affiliates.yml b/domain_access/config/install/field.storage.user.field_domain_all_affiliates.yml index a6f9292e..e8f9888f 100644 --- a/domain_access/config/install/field.storage.user.field_domain_all_affiliates.yml +++ b/domain_access/config/install/field.storage.user.field_domain_all_affiliates.yml @@ -4,6 +4,9 @@ dependencies: module: - domain - user + enforced: + module: + - domain_access id: user.field_domain_all_affiliates field_name: field_domain_all_affiliates entity_type: user diff --git a/domain_access/config/schema/domain_access.schema.yml b/domain_access/config/schema/domain_access.schema.yml index dcdcc11d..63676c66 100644 --- a/domain_access/config/schema/domain_access.schema.yml +++ b/domain_access/config/schema/domain_access.schema.yml @@ -5,6 +5,9 @@ domain_access.settings: node_advanced_tab: type: boolean label: 'Move domain access field to a tab in the advanced settings on nodes.' + node_advanced_tab_open: + type: boolean + label: 'Open the Domain Access details by default.' action.configuration.domain_access_all_action: type: action_configuration_default label: 'Assign content to all affiliates' diff --git a/domain_access/config/schema/domain_access.views.schema.yml b/domain_access/config/schema/domain_access.views.schema.yml new file mode 100644 index 00000000..e489a922 --- /dev/null +++ b/domain_access/config/schema/domain_access.views.schema.yml @@ -0,0 +1,63 @@ +# Schema for the domain access plugins. + +views.access.domain_access_admin: + type: mapping + label: 'Domain Access: Administer domain editors' +views.access.domain_access_editor: + type: mapping + label: 'Domain Access: Edit domain content' +views.argument.domain_access_argument: + type: views_argument + label: 'Domain Access' +views.field.domain_access_field: + type: views_field + label: 'Domain Access' + mapping: + click_sort_column: + type: string + label: 'Column used for click sorting' + type: + type: string + label: 'Formatter' + settings: + label: 'Settings' + type: field.formatter.settings.[%parent.type] + group_column: + type: string + label: 'Group by column' + group_columns: + type: sequence + label: 'Group by columns' + sequence: + type: string + label: 'Column' + group_rows: + type: boolean + label: 'Display all values in the same row' + delta_limit: + type: integer + label: 'Field' + delta_offset: + type: integer + label: 'Offset' + delta_reversed: + type: boolean + label: 'Reversed' + delta_first_last: + type: boolean + label: 'First and last only' + multi_type: + type: string + label: 'Display type' + separator: + type: label + label: 'Separator' + field_api_classes: + type: boolean + label: 'Use field template' +views.filter.domain_access_current_all_filter: + type: views_filter + label: 'Current Domain or All Domains' +views.filter.domain_access_filter: + type: views.filter.in_operator + label: 'Domain Access' diff --git a/domain_access/domain_access.info.yml b/domain_access/domain_access.info.yml index 49270f82..98731eca 100644 --- a/domain_access/domain_access.info.yml +++ b/domain_access/domain_access.info.yml @@ -2,8 +2,13 @@ name: Domain Access description: 'Domain-based access control for content.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - node - - domain + - drupal:node + - domain:domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_access/domain_access.install b/domain_access/domain_access.install index 67d760d3..189ea8ff 100644 --- a/domain_access/domain_access.install +++ b/domain_access/domain_access.install @@ -12,6 +12,11 @@ * files because we have an unknown number of node types. */ function domain_access_install() { + if (\Drupal::isConfigSyncing()) { + // Configuration is assumed to already be checked by the config importer + // validation events. + return; + } // Assign domain access to bundles. $list['user'] = 'user'; @@ -24,29 +29,18 @@ function domain_access_install() { domain_access_confirm_fields($entity_type, $bundle); } // Install our actions. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { domain_access_domain_insert($domain); } } /** - * Implements hook_uninstall(). - * - * Removes access control fields on uninstall. + * Add the setting to open the domain access fieldset. */ -function domain_access_uninstall() { - $field_storage_config = \Drupal::entityTypeManager() - ->getStorage('field_storage_config'); - - foreach (array('node', 'user') as $type) { - $id = $type . '.' . DOMAIN_ACCESS_FIELD; - if ($field = $field_storage_config->load($id)) { - $field->delete(); - } - $id = $type . '.' . DOMAIN_ACCESS_ALL_FIELD; - if ($field = $field_storage_config->load($id)) { - $field->delete(); - } - } +function domain_access_update_8001() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('domain_access.settings'); + $config->set('node_advanced_tab_open', 0); + $config->save(TRUE); } diff --git a/domain_access/domain_access.module b/domain_access/domain_access.module index 9c95b53d..5824a316 100755 --- a/domain_access/domain_access.module +++ b/domain_access/domain_access.module @@ -11,17 +11,22 @@ use Drupal\node\NodeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\user\Entity\User; use Drupal\Core\Form\FormState; -use Drupal\Core\Form\FormStateInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * The name of the node access control field. + * + * @deprecated This constant will be replaced in the final release by + * Drupal\domain\DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD. */ const DOMAIN_ACCESS_FIELD = 'field_domain_access'; /** * The name of the all affiliates field. + * + * @deprecated This constant will be replaced in the final release by + * Drupal\domain\DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD. */ const DOMAIN_ACCESS_ALL_FIELD = 'field_domain_all_affiliates'; @@ -29,12 +34,12 @@ const DOMAIN_ACCESS_ALL_FIELD = 'field_domain_all_affiliates'; * Implements hook_node_grants(). */ function domain_access_node_grants(AccountInterface $account, $op) { - $grants = array(); + $grants = []; /** @var \Drupal\domain\Entity\Domain $active */ $active = \Drupal::service('domain.negotiator')->getActiveDomain(); if (empty($active)) { - $active = \Drupal::service('domain.loader')->loadDefaultDomain(); + $active = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultDomain(); } // No domains means no permissions. @@ -53,18 +58,18 @@ function domain_access_node_grants(AccountInterface $account, $op) { $grants['domain_id'][] = $id; $grants['domain_site'][] = 0; if ($user->hasPermission('view unpublished domain content')) { - if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DOMAIN_ACCESS_ALL_FIELD)->value)) { + if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value)) { $grants['domain_unpublished'][] = $id; } } } elseif ($op == 'update' && $user->hasPermission('edit domain content')) { - if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DOMAIN_ACCESS_ALL_FIELD)->value)) { + if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value)) { $grants['domain_id'][] = $id; } } elseif ($op == 'delete' && $user->hasPermission('delete domain content')) { - if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DOMAIN_ACCESS_ALL_FIELD)->value)) { + if ($user->hasPermission('publish to any domain') || in_array($id, $user_domains) || !empty($user->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value)) { $grants['domain_id'][] = $id; } } @@ -75,7 +80,7 @@ function domain_access_node_grants(AccountInterface $account, $op) { * Implements hook_node_access_records(). */ function domain_access_node_access_records(NodeInterface $node) { - $grants = array(); + $grants = []; // Create grants for each translation of the node. See the report at // https://www.drupal.org/node/2825419 for the logic here. Note that right // now, grants may not be the same for all languages. @@ -90,40 +95,39 @@ function domain_access_node_access_records(NodeInterface $node) { } foreach ($domains as $id => $domainId) { /** @var \Drupal\domain\DomainInterface $domain */ - if ($domain = \Drupal::service('domain.loader')->load($id)) { - $grants[] = array( + if ($domain = \Drupal::entityTypeManager()->getStorage('domain')->load($id)) { + $grants[] = [ 'realm' => ($translation->isPublished()) ? 'domain_id' : 'domain_unpublished', 'gid' => $domain->getDomainId(), 'grant_view' => 1, 'grant_update' => 1, 'grant_delete' => 1, 'langcode' => $langcode, - ); + ]; } } // Set the domain_site grant. - if (!empty($translation->get(DOMAIN_ACCESS_ALL_FIELD)->value) && $translation->isPublished()) { - $grants[] = array( + if ($translation->hasField(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD) && !empty($translation->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value) && $translation->isPublished()) { + $grants[] = [ 'realm' => 'domain_site', 'gid' => 0, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0, 'langcode' => $langcode, - ); + ]; } // Because of language translation, we must save a record for each language. - // Note that the gid of 1 is never allowed for domain_site in - // domain_node_grants(). - elseif (count($translations) > 1) { - $grants[] = array( + // even if that record adds no permissions, as this one does. + else { + $grants[] = [ 'realm' => 'domain_site', 'gid' => 1, - 'grant_view' => 1, + 'grant_view' => 0, 'grant_update' => 0, 'grant_delete' => 0, 'langcode' => $langcode, - ); + ]; } } return $grants; @@ -153,13 +157,17 @@ function domain_access_user_presave(EntityInterface $account) { * Handles presave operations for devel generate. */ function domain_access_presave_generate(EntityInterface $entity) { + if (!$entity->hasField(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)) { + return; + } + // There is a core bug https://www.drupal.org/node/2609252 that causes a - // fatal database errors if the boolean DOMAIN_ACCESS_ALL_FIELD is set when + // fatal database errors if the boolean DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD is set when // a user cannot access the field. See domain_access_entity_field_access(). // To overcome this issue, we cast the boolean to integer, which prevents the // failure. - $value = (int) $entity->get(DOMAIN_ACCESS_ALL_FIELD)->value; - $entity->set(DOMAIN_ACCESS_ALL_FIELD, $value); + $value = (int) $entity->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value; + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, $value); // Handle devel module settings. $exists = \Drupal::moduleHandler()->moduleExists('devel_generate'); @@ -169,20 +177,20 @@ function domain_access_presave_generate(EntityInterface $entity) { if (isset($entity->devel_generate['domain_access'])) { $selection = array_filter($entity->devel_generate['domain_access']); if (isset($selection['random-selection'])) { - $domains = \Drupal::service('domain.loader')->loadMultiple(); - $values[DOMAIN_ACCESS_FIELD] = array_rand($domains, ceil(rand(1, count($domains)))); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $values[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD] = array_rand($domains, ceil(rand(1, count($domains)))); } else { - $values[DOMAIN_ACCESS_FIELD] = array_keys($selection); + $values[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD] = array_keys($selection); } } if (isset($entity->devel_generate['domain_all'])) { $selection = $entity->devel_generate['domain_all']; if ($selection == 'random-selection') { - $values[DOMAIN_ACCESS_ALL_FIELD] = rand(0, 1); + $values[DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD] = rand(0, 1); } else { - $values[DOMAIN_ACCESS_ALL_FIELD] = ($selection = 'yes' ? 1 : 0); + $values[DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD] = ($selection = 'yes' ? 1 : 0); } } foreach ($values as $name => $value) { @@ -201,8 +209,8 @@ function domain_access_form_devel_generate_form_content_alter(&$form, &$form_sta // Add our element to the Devel generate form. $form['submit']['#weight'] = 10; $list = ['random-selection' => t('Random selection')]; - $list += \Drupal::service('domain.loader')->loadOptionsList(); - $form['domain_access'] = array( + $list += \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(); + $form['domain_access'] = [ '#title' => t('Domains'), '#type' => 'checkboxes', '#options' => $list, @@ -211,8 +219,8 @@ function domain_access_form_devel_generate_form_content_alter(&$form, &$form_sta '#size' => count($list) > 5 ? 5 : count($list), '#default_value' => ['random-selection'], '#description' => t('Sets the domains for created nodes. Random selection overrides other choices.'), - ); - $form['domain_all'] = array( + ]; + $form['domain_all'] = [ '#title' => t('Send to all affiliates'), '#type' => 'radios', '#options' => [ @@ -223,7 +231,7 @@ function domain_access_form_devel_generate_form_content_alter(&$form, &$form_sta '#default_value' => 'random-selection', '#weight' => 3, '#description' => t('Sets visibility across all affiliates.'), - ); + ]; } /** @@ -243,31 +251,32 @@ function domain_access_form_devel_generate_form_user_alter(&$form, &$form_state, function domain_access_form_node_form_alter(&$form, FormState $form_state, $form_id) { $move_enabled = \Drupal::config('domain_access.settings')->get('node_advanced_tab'); if ( - $move_enabled && isset($form[DOMAIN_ACCESS_FIELD]) && - isset($form[DOMAIN_ACCESS_ALL_FIELD]) && - empty($form[DOMAIN_ACCESS_FIELD]['#group']) && - empty($form[DOMAIN_ACCESS_ALL_FIELD]['#group']) + $move_enabled && isset($form[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]) && + isset($form[DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD]) && + empty($form[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['#group']) && + empty($form[DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD]['#group']) ) { - // Move to the tabs on the entity form - $form[DOMAIN_ACCESS_FIELD]['#group'] = 'domain'; - $form[DOMAIN_ACCESS_ALL_FIELD]['#group'] = 'domain'; + // Move to the tabs on the entity form. + $form[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['#group'] = 'domain'; + $form[DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD]['#group'] = 'domain'; $form['domain'] = [ '#type' => 'details', + '#open' => (bool) \Drupal::config('domain_access.settings')->get('node_advanced_tab_open'), '#title' => t('Domain settings'), '#group' => 'advanced', '#attributes' => [ - 'class' => ['node-form-options'] + 'class' => ['node-form-options'], ], '#attached' => [ 'library' => ['node/drupal.node'], ], '#weight' => 100, - '#optional' => TRUE + '#optional' => TRUE, ]; } // Add the options hidden from the user silently to the form. $manager = \Drupal::service('domain.element_manager'); - $form = $manager->setFormOptions($form, $form_state, DOMAIN_ACCESS_FIELD); + $form = $manager->setFormOptions($form, $form_state, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); } /** @@ -278,7 +287,7 @@ function domain_access_form_node_form_alter(&$form, FormState $form_state, $form function domain_access_form_user_form_alter(&$form, &$form_state, $form_id) { // Add the options hidden from the user silently to the form. $manager = \Drupal::service('domain.element_manager'); - $form = $manager->setFormOptions($form, $form_state, DOMAIN_ACCESS_FIELD); + $form = $manager->setFormOptions($form, $form_state, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); } /** @@ -295,7 +304,7 @@ function domain_access_domain_references_alter($query, $account, $context) { break; } elseif ($account->hasPermission('publish to any assigned domain')) { - if (!empty($account->get(DOMAIN_ACCESS_ALL_FIELD)->value)) { + if (!empty($account->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value)) { break; } $allowed = \Drupal::service('domain_access.manager')->getAccessValues($account); @@ -312,7 +321,7 @@ function domain_access_domain_references_alter($query, $account, $context) { // Do nothing. } elseif ($account->hasPermission('assign domain editors')) { - if (!empty($account->get(DOMAIN_ACCESS_ALL_FIELD)->value)) { + if (!empty($account->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value)) { break; } $allowed = \Drupal::service('domain_access.manager')->getAccessValues($account); @@ -345,21 +354,22 @@ function domain_access_node_access(NodeInterface $node, $op, AccountInterface $a } // Check to see that we have a valid active domain. // Without one, we cannot assert an opinion about access. - if (empty($active_domain->getDomainId())) { - return AccessResult::neutral(); + if (!$active_domain || empty($active_domain->getDomainId())) { + return AccessResult::neutral()->addCacheableDependency($node); } $type = $node->bundle(); $manager = \Drupal::service('domain_access.manager'); $allowed = FALSE; - // In order to access update or delete, the user must be able to View. - if ($op == 'view' && $manager->checkEntityAccess($node, $account)) { - /** @var \Drupal\user\UserInterface $user */ + // In order to access update or delete, the user must be able to view. + // Domain-specific permissions are relevant only if the node is not published. + if ($op == 'view') { if ($node->isPublished()) { - $allowed = TRUE; + // Explicit restatement of the condition, for clarity. + $allowed = FALSE; } - elseif ($account->hasPermission('view unpublished domain content')) { + elseif ($account->hasPermission('view unpublished domain content') && $manager->checkEntityAccess($node, $account)) { $allowed = TRUE; } } @@ -389,8 +399,8 @@ function domain_access_node_access(NodeInterface $node, $op, AccountInterface $a ->addCacheableDependency($node); } - // No opinion. - return AccessResult::neutral(); + // No opinion on FALSE. + return AccessResult::neutral()->addCacheableDependency($node); } /** @@ -423,47 +433,49 @@ function domain_access_node_create_access(AccountInterface $account, $context, $ /** * Implements hook_entity_field_access(). + * + * Hides the domain access fields from the entity add/edit forms + * when the user cannot access them. */ function domain_access_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { - // Hide the domain access fields from the entity add/edit forms - // when the user cannot access them. - if ($operation != 'edit') { + // If not editing an entity, do nothing. + if ($operation !== 'edit' || empty($items)) { return AccessResult::neutral(); } // The entity the field is attached to. $entity = $items->getEntity(); - if ($field_definition->getName() == DOMAIN_ACCESS_FIELD) { - if ($entity instanceof User) { + if ($field_definition->getName() == DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD) { + if ($entity instanceof AccountInterface) { $access = AccessResult::allowedIfHasPermissions($account, [ 'assign domain editors', 'assign editors to any domain', ], 'OR'); } - else { + elseif ($entity instanceof NodeInterface) { // Treat any other entity as content. $access = AccessResult::allowedIfHasPermissions($account, [ 'publish to any domain', 'publish to any assigned domain', ], 'OR'); } - // allowedIfHasPermissions returns allowed() or neutral(). // In this case, we want it to be forbidden, // if user doesn't have the permissions above. - if (!$access->isAllowed()) { + if (isset($access) && !$access->isAllowed()) { return AccessResult::forbidden(); } } // Check permissions on the All Affiliates field. - elseif ($field_definition->getName() == DOMAIN_ACCESS_ALL_FIELD) { - if ($entity instanceof User) { + elseif ($field_definition->getName() == DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD) { + if ($entity instanceof AccountInterface) { return AccessResult::forbiddenIf(!$account->hasPermission('assign editors to any domain')); } - - // Treat any other entity as content. - return AccessResult::forbiddenIf(!$account->hasPermission('publish to any domain')); + elseif ($entity instanceof NodeInterface) { + // Treat any other entity as content. + return AccessResult::forbiddenIf(!$account->hasPermission('publish to any domain')); + } } return AccessResult::neutral(); @@ -475,7 +487,24 @@ function domain_access_entity_field_access($operation, FieldDefinitionInterface * Creates our fields when new node types are created. */ function domain_access_node_type_insert(EntityInterface $entity) { - domain_access_confirm_fields('node', $entity->id()); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + if (!$entity->isSyncing()) { + // Do not fire hook when config sync in progress. + domain_access_confirm_fields('node', $entity->id()); + } +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + * + * In some cases, form display modes are not set when the node type is created. + * be sure to update our field definitions on creation of form_display for + * node types. + */ +function domain_access_entity_form_display_insert(EntityInterface $entity) { + if (!$entity->isSyncing() && $entity->getTargetEntityTypeId() == 'node' && $bundle = $entity->getTargetBundle()) { + domain_access_confirm_fields('node', $bundle); + } } /** @@ -494,15 +523,16 @@ function domain_access_node_type_insert(EntityInterface $entity) { * If calling this function for entities other than user or node, it is the * caller's responsibility to provide this text. * - * This function is here for convenience during installation. It is not really - * an API function. Modules wishing to add fields to non-node entities must - * provide their own field storage. See the field storage YML sample in - * tests/modules/domain_access_test for an example of field storage definitions. + * This function is here for convenience during installation. It is not really + * an API function. Modules wishing to add fields to non-node entities must + * provide their own field storage. See the field storage YML sample in + * tests/modules/domain_access_test for an example of field storage + * definitions. * * @see domain_access_node_type_insert() * @see domain_access_install() */ -function domain_access_confirm_fields($entity_type, $bundle, $text = array()) { +function domain_access_confirm_fields($entity_type, $bundle, array $text = []) { // We have reports that importing config causes this function to fail. try { $text['node'] = [ @@ -516,11 +546,12 @@ function domain_access_confirm_fields($entity_type, $bundle, $text = array()) { 'description' => 'Make this user an editor on all domains.', ]; - $id = $entity_type . '.' . $bundle . '.' . DOMAIN_ACCESS_FIELD; + $id = $entity_type . '.' . $bundle . '.' . DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD; - if (!$field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { - $field = array( - 'field_name' => DOMAIN_ACCESS_FIELD, + $field_storage = \Drupal::entityTypeManager()->getStorage('field_config'); + if (!$field = $field_storage->load($id)) { + $field = [ + 'field_name' => DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, 'entity_type' => $entity_type, 'label' => 'Domain Access', 'bundle' => $bundle, @@ -528,44 +559,45 @@ function domain_access_confirm_fields($entity_type, $bundle, $text = array()) { 'required' => $entity_type !== 'user', 'description' => 'Select the affiliate domain(s) for this ' . $text[$entity_type]['name'], 'default_value_callback' => 'Drupal\domain_access\DomainAccessManager::getDefaultValue', - 'settings' => array( - 'handler_settings' => array( - 'sort' => array('field' => 'weight', 'direction' => 'ASC'), - ), - ), - ); - $field_config = \Drupal::entityTypeManager()->getStorage('field_config')->create($field); + 'settings' => [ + 'handler' => 'default:domain', + // Handler_settings are deprecated but seem to be necessary here. + 'handler_settings' => [ + 'target_bundles' => NULL, + 'sort' => ['field' => 'weight', 'direction' => 'ASC'], + ], + 'target_bundles' => NULL, + 'sort' => ['field' => 'weight', 'direction' => 'ASC'], + ], + ]; + $field_config = $field_storage->create($field); $field_config->save(); } // Assign the all affiliates field. - $id = $entity_type . '.' . $bundle . '.' . DOMAIN_ACCESS_ALL_FIELD; - if (!$field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { - $field = array( - 'field_name' => DOMAIN_ACCESS_ALL_FIELD, + $id = $entity_type . '.' . $bundle . '.' . DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD; + if (!$field = $field_storage->load($id)) { + $field = [ + 'field_name' => DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, 'entity_type' => $entity_type, 'label' => $text[$entity_type]['label'], 'bundle' => $bundle, 'required' => FALSE, 'description' => $text[$entity_type]['description'], - 'default_value_callback' => 'Drupal\domain_access\DomainAccessManager::getDefaultAllValue', - ); - $field_config = \Drupal::entityTypeManager()->getStorage('field_config')->create($field); + ]; + $field_config = $field_storage->create($field); $field_config->save(); } // Tell the form system how to behave. Default to radio buttons. - // @TODO: This function is deprecated, but using the OO syntax is causing - // test fails. - entity_get_form_display($entity_type, $bundle, 'default') - ->setComponent(DOMAIN_ACCESS_FIELD, array( + if ($display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load($entity_type . '.' . $bundle . '.default')) { + $display->setComponent(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, [ 'type' => 'options_buttons', 'weight' => 40, - )) - ->setComponent(DOMAIN_ACCESS_ALL_FIELD, array( + ])->setComponent(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, [ 'type' => 'boolean_checkbox', - 'settings' => array('display_label' => 1), + 'settings' => ['display_label' => 1], 'weight' => 41, - )) - ->save(); + ])->save(); + } } catch (Exception $e) { \Drupal::logger('domain_access')->notice('Field installation failed.'); @@ -576,88 +608,99 @@ function domain_access_confirm_fields($entity_type, $bundle, $text = array()) { * Implements hook_views_data_alter(). */ function domain_access_views_data_alter(array &$data) { - $table = 'node__' . DOMAIN_ACCESS_FIELD; - $data[$table][DOMAIN_ACCESS_FIELD]['field']['id'] = 'domain_access_field'; - $data[$table][DOMAIN_ACCESS_FIELD . '_target_id']['filter']['id'] = 'domain_access_filter'; - $data[$table][DOMAIN_ACCESS_FIELD . '_target_id']['argument']['id'] = 'domain_access_argument'; + $table = 'node__' . DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['field']['id'] = 'domain_access_field'; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '_target_id']['filter']['id'] = 'domain_access_filter'; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '_target_id']['argument']['id'] = 'domain_access_argument'; // Current domain filter. - $data[$table]['current_all'] = array( + $data[$table]['current_all'] = [ 'title' => t('Current domain'), 'group' => t('Domain'), - 'filter' => array( - 'field' => DOMAIN_ACCESS_FIELD . '_target_id', + 'filter' => [ + 'field' => DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '_target_id', 'id' => 'domain_access_current_all_filter', 'title' => t('Available on current domain'), - 'help' => t('Filters out nodes not available on current domain (published to current domain or all affiliates).'), - ), - ); - - $table = 'user__' . DOMAIN_ACCESS_FIELD; - $data[$table][DOMAIN_ACCESS_FIELD]['field']['id'] = 'domain_access_field'; - $data[$table][DOMAIN_ACCESS_FIELD . '_target_id']['filter']['id'] = 'domain_access_filter'; - $data[$table][DOMAIN_ACCESS_FIELD . '_target_id']['argument']['id'] = 'domain_access_argument'; + 'help' => t('Filters out nodes available on current domain (published to current domain or all affiliates).'), + ], + ]; + // Since domains are not stored in the database, relationships cannot be used. + unset($data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['relationship']); + + // Set the user data. + $table = 'user__' . DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['field']['id'] = 'domain_access_field'; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '_target_id']['filter']['id'] = 'domain_access_filter'; + $data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '_target_id']['argument']['id'] = 'domain_access_argument'; + + // Since domains are not stored in the database, relationships cannot be used. + unset($data[$table][DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]['relationship']); } /** * Implements hook_ENTITY_TYPE_insert(). */ function domain_access_domain_insert($entity) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + if ($entity->isSyncing()) { + // Do not fire hook when config sync in progress. + return; + } $id = 'domain_access_add_action.' . $entity->id(); $controller = \Drupal::entityTypeManager()->getStorage('action'); if (!$controller->load($id)) { /** @var \Drupal\system\Entity\Action $action */ - $action = $controller->create(array( + $action = $controller->create([ 'id' => $id, 'type' => 'node', - 'label' => t('Add selected content to the @label domain', array('@label' => $entity->label())), - 'configuration' => array( + 'label' => t('Add selected content to the @label domain', ['@label' => $entity->label()]), + 'configuration' => [ 'domain_id' => $entity->id(), - ), + ], 'plugin' => 'domain_access_add_action', - )); + ]); $action->trustData()->save(); } $remove_id = 'domain_access_remove_action.' . $entity->id(); if (!$controller->load($remove_id)) { /** @var \Drupal\system\Entity\Action $action */ - $action = $controller->create(array( + $action = $controller->create([ 'id' => $remove_id, 'type' => 'node', - 'label' => t('Remove selected content from the @label domain', array('@label' => $entity->label())), - 'configuration' => array( + 'label' => t('Remove selected content from the @label domain', ['@label' => $entity->label()]), + 'configuration' => [ 'domain_id' => $entity->id(), - ), + ], 'plugin' => 'domain_access_remove_action', - )); + ]); $action->trustData()->save(); } $id = 'domain_access_add_editor_action.' . $entity->id(); if (!$controller->load($id)) { /** @var \Drupal\system\Entity\Action $action */ - $action = $controller->create(array( + $action = $controller->create([ 'id' => $id, 'type' => 'user', - 'label' => t('Add editors to the @label domain', array('@label' => $entity->label())), - 'configuration' => array( + 'label' => t('Add editors to the @label domain', ['@label' => $entity->label()]), + 'configuration' => [ 'domain_id' => $entity->id(), - ), + ], 'plugin' => 'domain_access_add_editor_action', - )); + ]); $action->trustData()->save(); } $remove_id = 'domain_access_remove_editor_action.' . $entity->id(); if (!$controller->load($remove_id)) { /** @var \Drupal\system\Entity\Action $action */ - $action = $controller->create(array( + $action = $controller->create([ 'id' => $remove_id, 'type' => 'user', - 'label' => t('Remove editors from the @label domain', array('@label' => $entity->label())), - 'configuration' => array( + 'label' => t('Remove editors from the @label domain', ['@label' => $entity->label()]), + 'configuration' => [ 'domain_id' => $entity->id(), - ), + ], 'plugin' => 'domain_access_remove_editor_action', - )); + ]); $action->trustData()->save(); } } @@ -667,12 +710,12 @@ function domain_access_domain_insert($entity) { */ function domain_access_domain_delete(EntityInterface $entity) { $controller = \Drupal::entityTypeManager()->getStorage('action'); - $actions = $controller->loadMultiple(array( + $actions = $controller->loadMultiple([ 'domain_access_add_action.' . $entity->id(), 'domain_access_remove_action.' . $entity->id(), 'domain_access_add_editor_action.' . $entity->id(), 'domain_access_remove_editor_action.' . $entity->id(), - )); + ]); foreach ($actions as $action) { $action->delete(); } @@ -685,7 +728,7 @@ function domain_access_domain_delete(EntityInterface $entity) { * default values properly. Note that here we just care if the form saves an * entity. We then pass that entity to a helper function. * - * @see domain_access_default_form_values(). + * @see domain_access_default_form_values() */ function domain_access_form_alter(&$form, &$form_state, $form_id) { if ($object = $form_state->getFormObject() && !empty($object) && is_callable([$object, 'getEntity']) && $entity = $object->getEntity()) { @@ -699,7 +742,7 @@ function domain_access_form_alter(&$form, &$form_state, $form_id) { * This function is a workaround for a core bug. When the domain access field * is not accessible to some users, the existing values are not preserved. * - * @see domain_access_entity_field_access(). + * @see domain_access_entity_field_access() */ function domain_access_default_form_values(&$form, &$form_state, $entity) { // Set domain access default value when the user does not have access diff --git a/domain_access/domain_access.services.yml b/domain_access/domain_access.services.yml index 420e2a57..3ae6c385 100644 --- a/domain_access/domain_access.services.yml +++ b/domain_access/domain_access.services.yml @@ -1,6 +1,9 @@ services: domain_access.manager: class: Drupal\domain_access\DomainAccessManager + arguments: ['@domain.negotiator', '@module_handler', '@entity_type.manager'] + access_check.domain_access_views: + class: Drupal\domain_access\Access\DomainAccessViewsAccess + arguments: ['@entity_type.manager', '@domain_access.manager'] tags: - - { name: persist } - arguments: ['@domain.loader', '@domain.negotiator'] + - { name: access_check, applies_to: _domain_access_views } diff --git a/domain_access/drush.services.yml b/domain_access/drush.services.yml new file mode 100644 index 00000000..e7541c0e --- /dev/null +++ b/domain_access/drush.services.yml @@ -0,0 +1,5 @@ +services: + domain_access.commands: + class: \Drupal\domain_access\Commands\DomainAccessCommands + tags: + - { name: drush.command } diff --git a/domain_access/src/Access/DomainAccessViewsAccess.php b/domain_access/src/Access/DomainAccessViewsAccess.php new file mode 100644 index 00000000..783e61b2 --- /dev/null +++ b/domain_access/src/Access/DomainAccessViewsAccess.php @@ -0,0 +1,105 @@ +entityTypeManager = $entity_type_manager; + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); + $this->userStorage = $this->entityTypeManager->getStorage('user'); + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, AccountInterface $account, $arg_0 = NULL) { + // Permissions are stored on the route defaults. + $permission = $route->getDefault('domain_permission'); + $allPermission = $route->getDefault('domain_all_permission'); + + // Users with this permission can see any domain content lists, and it is + // required to view all affiliates. + if ($account->hasPermission($allPermission)) { + return AccessResult::allowed(); + } + + // Load the domain from the passed argument. In testing, this passed NULL + // in some instances. + if (!is_null($arg_0)) { + $domain = $this->domainStorage->load($arg_0); + } + + // Domain found, check user permissions. + if (!empty($domain)) { + if ($this->manager->hasDomainPermissions($account, $domain, [$permission])) { + return AccessResult::allowed(); + } + } + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function applies(Route $route) { + return $route->hasRequirement($this->requirementsKey); + } + +} diff --git a/domain_access/src/Commands/DomainAccessCommands.php b/domain_access/src/Commands/DomainAccessCommands.php new file mode 100644 index 00000000..d127c299 --- /dev/null +++ b/domain_access/src/Commands/DomainAccessCommands.php @@ -0,0 +1,80 @@ +getFieldEntities(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + return $result; + } + +/** + * @hook option domain:delete + */ + public function deleteOptions(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'content-assign', + '', + InputOption::VALUE_OPTIONAL, + 'Reassign content for Domain Access', + null + ); + } + +/** + * @hook on-event domain-delete + */ + public function domainAccessDomainDelete($target_domain, $options) { + // Run our own deletion routine here. + if (is_null($options['content-assign'])) { + $policy_content = 'prompt'; + } + if (!empty($options['content-assign'])) { + if (in_array($options['content-assign'], $this->reassignment_policies, TRUE)) { + $policy_content = $options['content-assign']; + } + } + + $delete_options = [ + 'entity_filter' => 'node', + 'policy' => $policy_content, + 'field' => DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, + ]; + + return $this->doReassign($target_domain, $delete_options); + } + +} diff --git a/domain_access/src/DomainAccessManager.php b/domain_access/src/DomainAccessManager.php index a9516e12..4cc55f9e 100644 --- a/domain_access/src/DomainAccessManager.php +++ b/domain_access/src/DomainAccessManager.php @@ -2,13 +2,13 @@ namespace Drupal\domain_access; -use Drupal\domain\DomainLoaderInterface; -use Drupal\domain\DomainNegotiatorInterface; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\domain\DomainInterface; +use Drupal\domain\DomainNegotiatorInterface; /** * Checks the access status of entities based on domain settings. @@ -19,45 +19,67 @@ class DomainAccessManager implements DomainAccessManagerInterface { /** - * @var \Drupal\domain\DomainLoaderInterface + * The domain negotiator. + * + * @var \Drupal\domain\DomainNegotiatorInterface */ - protected $loader; + protected $negotiator; /** - * @var \Drupal\domain\DomainNegotiatorInterface + * The Drupal module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface */ - protected $negotiator; + protected $moduleHandler; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The domain storage. + * + * @var \Drupal\domain\DomainStorageInterface + */ + protected $domainStorage; /** * Constructs a DomainAccessManager object. * - * @param \Drupal\domain\DomainLoaderInterface $loader - * The domain loader. * @param \Drupal\domain\DomainNegotiatorInterface $negotiator * The domain negotiator. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The Drupal module handler. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(DomainLoaderInterface $loader, DomainNegotiatorInterface $negotiator) { - $this->loader = $loader; + public function __construct(DomainNegotiatorInterface $negotiator, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager) { $this->negotiator = $negotiator; + $this->moduleHandler = $module_handler; + $this->entityTypeManager = $entity_type_manager; + $this->domainStorage = $entity_type_manager->getStorage('domain'); } /** - * @inheritdoc + * {@inheritdoc} */ - public function getAccessValues(EntityInterface $entity, $field_name = DOMAIN_ACCESS_FIELD) { + public static function getAccessValues(FieldableEntityInterface $entity, $field_name = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD) { // @TODO: static cache. - $list = array(); + $list = []; // @TODO In tests, $entity is returning NULL. if (is_null($entity)) { return $list; } // Get the values of an entity. - $values = $entity->get($field_name); + $values = $entity->hasField($field_name) ? $entity->get($field_name) : NULL; // Must be at least one item. if (!empty($values)) { foreach ($values as $item) { if ($target = $item->getValue()) { - if ($domain = $this->loader->load($target['target_id'])) { + if ($domain = \Drupal::entityTypeManager()->getStorage('domain')->load($target['target_id'])) { $list[$domain->id()] = $domain->getDomainId(); } } @@ -67,16 +89,16 @@ public function getAccessValues(EntityInterface $entity, $field_name = DOMAIN_AC } /** - * @inheritdoc + * {@inheritdoc} */ - public function getAllValue(EntityInterface $entity) { - return $entity->get(DOMAIN_ACCESS_ALL_FIELD)->value; + public static function getAllValue(FieldableEntityInterface $entity) { + return $entity->hasField(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD) ? $entity->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value : NULL; } /** - * @inheritdoc + * {@inheritdoc} */ - public function checkEntityAccess(EntityInterface $entity, AccountInterface $account) { + public function checkEntityAccess(FieldableEntityInterface $entity, AccountInterface $account) { $entity_domains = $this->getAccessValues($entity); $user = \Drupal::entityTypeManager()->getStorage('user')->load($account->id()); if (!empty($this->getAllValue($user)) && !empty($entity_domains)) { @@ -87,53 +109,95 @@ public function checkEntityAccess(EntityInterface $entity, AccountInterface $acc } /** - * @inheritdoc + * {@inheritdoc} */ public static function getDefaultValue(FieldableEntityInterface $entity, FieldDefinitionInterface $definition) { - $item = array(); - switch ($entity->getEntityType()->id()) { - case 'user': - case 'node': - if ($entity->isNew()) { - /** @var \Drupal\domain\DomainInterface $active */ - if ($active = \Drupal::service('domain.negotiator')->getActiveDomain()) { - $item[0]['target_uuid'] = $active->uuid(); - } - } - // This code does not fire, but it should. - else { - foreach (self::getAccessValues($entity) as $id) { - $item[] = $id; - } - } - break; - default: - break; + $item = []; + if (!$entity->isNew()) { + // If set, ensure we do not drop existing data. + foreach (self::getAccessValues($entity) as $id) { + $item[] = $id; + } + } + // When creating a new entity, populate if required. + elseif ($entity->getFieldDefinition(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD)->isRequired()) { + /** @var \Drupal\domain\DomainInterface $active */ + if ($active = \Drupal::service('domain.negotiator')->getActiveDomain()) { + $item[0]['target_uuid'] = $active->uuid(); + } } return $item; } /** - * @inheritdoc + * {@inheritdoc} */ - public static function getDefaultAllValue(FieldableEntityInterface $entity, FieldDefinitionInterface $definition) { - // @TODO: This may become configurable. - $item = 0; - switch ($entity->getEntityType()) { - case 'user': - case 'node': - if ($entity->isNew()) { - $item = 0; + public function hasDomainPermissions(AccountInterface $account, DomainInterface $domain, array $permissions, $conjunction = 'AND') { + // Assume no access. + $access = FALSE; + + // In the case of multiple AND permissions, assume access and then deny if + // any check fails. + if ($conjunction == 'AND' && !empty($permissions)) { + $access = TRUE; + foreach ($permissions as $permission) { + if (!($permission_access = $account->hasPermission($permission))) { + $access = FALSE; + break; } - // This code does not fire, but it should. - else { - $item = self::getAllValue($entity); + } + } + // In the case of multiple OR permissions, assume deny and then allow if any + // check passes. + else { + foreach ($permissions as $permission) { + if ($permission_access = $account->hasPermission($permission)) { + $access = TRUE; + break; } - break; - default: - break; + } } - return $item; + // Validate that the user is assigned to the domain. If not, deny. + $user = \Drupal::entityTypeManager()->getStorage('user')->load($account->id()); + $allowed = $this->getAccessValues($user); + if (!isset($allowed[$domain->id()]) && empty($this->getAllValue($user))) { + $access = FALSE; + } + + return $access; + } + + /** + * {@inheritdoc} + */ + public function getContentUrls(FieldableEntityInterface $entity) { + $list = []; + $processed = FALSE; + $domains = $this->getAccessValues($entity); + if ($this->moduleHandler->moduleExists('domain_source')) { + $source = domain_source_get($entity); + if (isset($domains[$source])) { + unset($domains['source']); + } + if (!empty($source)) { + $list[] = $source; + } + $processed = TRUE; + } + $list = array_merge($list, array_keys($domains)); + $domains = $this->domainStorage->loadMultiple($list); + $urls = []; + foreach ($domains as $domain) { + $options['domain_target_id'] = $domain->id(); + $url = $entity->toUrl('canonical', $options); + if ($processed) { + $urls[$domain->id()] = $url->toString(); + } + else { + $urls[$domain->id()] = $domain->buildUrl($url->toString()); + } + } + return $urls; } } diff --git a/domain_access/src/DomainAccessManagerInterface.php b/domain_access/src/DomainAccessManagerInterface.php index 69905d7d..ed9384b6 100644 --- a/domain_access/src/DomainAccessManagerInterface.php +++ b/domain_access/src/DomainAccessManagerInterface.php @@ -2,21 +2,30 @@ namespace Drupal\domain_access; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\domain\DomainInterface; /** * Checks the access status of entities based on domain settings. */ interface DomainAccessManagerInterface { + /** + * The name of the node access control field. + */ + const DOMAIN_ACCESS_FIELD = 'field_domain_access'; + + /** + * The name of the all affiliates field. + */ + const DOMAIN_ACCESS_ALL_FIELD = 'field_domain_all_affiliates'; + /** * Get the domain access field values from an entity. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The entity to retrieve field data from. * @param string $field_name * The name of the field that holds our data. @@ -24,23 +33,23 @@ interface DomainAccessManagerInterface { * @return array * The domain access field values. */ - public function getAccessValues(EntityInterface $entity, $field_name = DOMAIN_ACCESS_FIELD); + public static function getAccessValues(FieldableEntityInterface $entity, $field_name = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); /** * Get the all affiliates field values from an entity. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The entity to retrieve field data from. * * @return bool * Returns TRUE if the entity is sent to all affiliates. */ - public function getAllValue(EntityInterface $entity); + public static function getAllValue(FieldableEntityInterface $entity); /** * Compare the entity values against a user's account assignments. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The entity being checked for access. * @param \Drupal\Core\Session\AccountInterface $account * The account of the user performing the action. @@ -48,7 +57,7 @@ public function getAllValue(EntityInterface $entity); * @return bool * Returns TRUE if the user has access to the domain. */ - public function checkEntityAccess(EntityInterface $entity, AccountInterface $account); + public function checkEntityAccess(FieldableEntityInterface $entity, AccountInterface $account); /** * Get the default field value for an entity. @@ -64,16 +73,33 @@ public function checkEntityAccess(EntityInterface $entity, AccountInterface $acc public static function getDefaultValue(FieldableEntityInterface $entity, FieldDefinitionInterface $definition); /** - * Get the default all affiliates value for an entity. + * Checks that a user belongs to the domain and has a set of permissions. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The user account. + * @param \Drupal\domain\DomainInterface $domain + * The domain being checked. + * @param array $permissions + * The relevant permissions to check. + * @param string $conjunction + * The conjunction AND|OR to use when checking permissions. + * + * @return bool + * Returns TRUE if the user is assigned to the domain and has the necessary + * permissions. + */ + public function hasDomainPermissions(AccountInterface $account, DomainInterface $domain, array $permissions, $conjunction = 'AND'); + + /** + * Get all possible URLs pointing to an entity. * * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity being created. - * @param \Drupal\Core\Field\FieldDefinitionInterface $definition - * The field being created. + * The entity to retrieve field data from. * * @return array - * The default all affiliates value(s). + * An array of absolute URLs keyed by domain_id, with an known canonical id + * as the first element of the array. */ - public static function getDefaultAllValue(FieldableEntityInterface $entity, FieldDefinitionInterface $definition); + public function getContentUrls(FieldableEntityInterface $entity); } diff --git a/domain_access/src/DomainAccessPermissions.php b/domain_access/src/DomainAccessPermissions.php index 2bb5f3f8..93341878 100644 --- a/domain_access/src/DomainAccessPermissions.php +++ b/domain_access/src/DomainAccessPermissions.php @@ -16,32 +16,35 @@ class DomainAccessPermissions { * Define permissions. */ public function permissions() { - $permissions = array( - 'assign domain editors' => array( + $permissions = [ + 'assign domain editors' => [ 'title' => $this->t('Assign additional editors to assigned domains'), - ), - 'assign editors to any domain' => array( + 'restrict access' => TRUE, + ], + 'assign editors to any domain' => [ 'title' => $this->t('Assign additional editors to any domains'), - ), - 'publish to any domain' => array( + 'restrict access' => TRUE, + ], + 'publish to any domain' => [ 'title' => $this->t('Publish to any domain'), - ), - 'publish to any assigned domain' => array( + ], + 'publish to any assigned domain' => [ 'title' => $this->t('Publish content to any assigned domain'), - ), - 'create domain content' => array( + ], + 'create domain content' => [ 'title' => $this->t('Create any content on assigned domains'), - ), - 'edit domain content' => array( + ], + 'edit domain content' => [ 'title' => $this->t('Edit any content on assigned domains'), - ), - 'delete domain content' => array( + ], + 'delete domain content' => [ 'title' => $this->t('Delete any content on assigned domains'), - ), - 'view unpublished domain content' => array( + ], + 'view unpublished domain content' => [ 'title' => $this->t('View unpublished content on assigned domains'), - ), - ); + 'restrict access' => TRUE, + ], + ]; // Generate standard node permissions for all applicable node types. foreach (NodeType::loadMultiple() as $type) { @@ -56,7 +59,7 @@ public function permissions() { * * Shamelessly lifted from node_list_permissions(). * - * @param NodeType $type + * @param \Drupal\node\Entity\NodeType $type * The node type object. * * @return array @@ -65,17 +68,17 @@ public function permissions() { private function nodePermissions(NodeType $type) { // Build standard list of node permissions for this type. $id = $type->id(); - $perms = array( - "create $id content on assigned domains" => array( - 'title' => $this->t('%type_name: Create new content on assigned domains', array('%type_name' => $type->label())), - ), - "update $id content on assigned domains" => array( - 'title' => $this->t('%type_name: Edit any content on assigned domains', array('%type_name' => $type->label())), - ), - "delete $id content on assigned domains" => array( - 'title' => $this->t('%type_name: Delete any content on assigned domains', array('%type_name' => $type->label())), - ), - ); + $perms = [ + "create $id content on assigned domains" => [ + 'title' => $this->t('%type_name: Create new content on assigned domains', ['%type_name' => $type->label()]), + ], + "update $id content on assigned domains" => [ + 'title' => $this->t('%type_name: Edit any content on assigned domains', ['%type_name' => $type->label()]), + ], + "delete $id content on assigned domains" => [ + 'title' => $this->t('%type_name: Delete any content on assigned domains', ['%type_name' => $type->label()]), + ], + ]; return $perms; } diff --git a/domain_access/src/Form/DomainAccessSettingsForm.php b/domain_access/src/Form/DomainAccessSettingsForm.php index f7cb2310..2aa0dc4d 100644 --- a/domain_access/src/Form/DomainAccessSettingsForm.php +++ b/domain_access/src/Form/DomainAccessSettingsForm.php @@ -1,15 +1,17 @@ config('domain_access.settings'); - $form['node_advanced_tab'] = array( + $form['node_advanced_tab'] = [ '#type' => 'checkbox', '#title' => $this->t('Move Domain Access fields to advanced node settings.'), '#default_value' => $config->get('node_advanced_tab'), '#description' => $this->t('When checked the Domain Access fields will be shown as a tab in the advanced settings on node edit form. However, if you have placed the fields in a field group already, they will not be moved.'), - ); + ]; + $form['node_advanced_tab_open'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Open the Domain Access details.'), + '#description' => $this->t('Set the details tab to be open by default.'), + '#default_value' => $config->get('node_advanced_tab_open'), + '#states' => [ + 'visible' => [ + ':input[name="node_advanced_tab"]' => ['checked' => TRUE], + ], + ], + ]; return parent::buildForm($form, $form_state); } @@ -43,11 +56,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('domain_access.settings') - ->set('node_advanced_tab', $form_state->getValue('node_advanced_tab')) + ->set('node_advanced_tab', (bool) $form_state->getValue('node_advanced_tab')) + ->set('node_advanced_tab_open', (bool) $form_state->getValue('node_advanced_tab_open')) ->save(); parent::submitForm($form, $form_state); } - } diff --git a/domain_access/src/Plugin/Action/DomainAccessActionBase.php b/domain_access/src/Plugin/Action/DomainAccessActionBase.php index a71c6c10..b9a3ab2f 100644 --- a/domain_access/src/Plugin/Action/DomainAccessActionBase.php +++ b/domain_access/src/Plugin/Action/DomainAccessActionBase.php @@ -40,7 +40,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('entity.manager')->getDefinition('domain') + $container->get('entity_type.manager')->getDefinition('domain') ); } @@ -48,23 +48,23 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function defaultConfiguration() { - return array( + return [ 'domain_id' => '', - ); + ]; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $domains = \Drupal::service('domain.loader')->loadOptionsList(); - $form['domain_id'] = array( + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(); + $form['domain_id'] = [ '#type' => 'checkboxes', '#title' => t('Domain'), '#options' => $domains, '#default_value' => $this->configuration['id'], '#required' => TRUE, - ); + ]; return $form; } diff --git a/domain_access/src/Plugin/Action/DomainAccessAdd.php b/domain_access/src/Plugin/Action/DomainAccessAdd.php index 5d388405..03ad8ba1 100644 --- a/domain_access/src/Plugin/Action/DomainAccessAdd.php +++ b/domain_access/src/Plugin/Action/DomainAccessAdd.php @@ -2,6 +2,8 @@ namespace Drupal\domain_access\Plugin\Action; +use Drupal\domain_access\DomainAccessManagerInterface; + /** * Assigns a node to a domain. * @@ -23,7 +25,7 @@ public function execute($entity = NULL) { // Add domain assignment if not present. if ($entity !== FALSE && !isset($node_domains[$id])) { $node_domains[$id] = $id; - $entity->set(DOMAIN_ACCESS_FIELD, array_keys($node_domains)); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, array_keys($node_domains)); $entity->save(); } } diff --git a/domain_access/src/Plugin/Action/DomainAccessAddEditor.php b/domain_access/src/Plugin/Action/DomainAccessAddEditor.php index e6e963de..733a6934 100644 --- a/domain_access/src/Plugin/Action/DomainAccessAddEditor.php +++ b/domain_access/src/Plugin/Action/DomainAccessAddEditor.php @@ -2,6 +2,8 @@ namespace Drupal\domain_access\Plugin\Action; +use Drupal\domain_access\DomainAccessManagerInterface; + /** * Assigns an editor to a domain. * @@ -23,7 +25,7 @@ public function execute($entity = NULL) { // Skip adding the role to the user if they already have it. if ($entity !== FALSE && !isset($user_domains[$id])) { $user_domains[$id] = $id; - $entity->set(DOMAIN_ACCESS_FIELD, array_keys($user_domains)); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, array_keys($user_domains)); $entity->save(); } } diff --git a/domain_access/src/Plugin/Action/DomainAccessAll.php b/domain_access/src/Plugin/Action/DomainAccessAll.php index 1695ddcd..e735e833 100644 --- a/domain_access/src/Plugin/Action/DomainAccessAll.php +++ b/domain_access/src/Plugin/Action/DomainAccessAll.php @@ -2,8 +2,7 @@ namespace Drupal\domain_access\Plugin\Action; -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Session\AccountInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Assigns a node to all affiliates. @@ -14,26 +13,14 @@ * type = "node" * ) */ -class DomainAccessAll extends ActionBase { +class DomainAccessAll extends DomainAccessActionBase { /** * {@inheritdoc} */ public function execute($entity = NULL) { - $entity->set(DOMAIN_ACCESS_ALL_FIELD, 1); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, 1); $entity->save(); } - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - /** @var \Drupal\node\NodeInterface $object */ - // @TODO: Check this logic. - $result = $object->access('update', $account, TRUE) - ->andIf($object->status->access('edit', $account, TRUE)); - - return $return_as_object ? $result : $result->isAllowed(); - } - } diff --git a/domain_access/src/Plugin/Action/DomainAccessEditAll.php b/domain_access/src/Plugin/Action/DomainAccessEditAll.php index 06e7f11c..686f5be0 100644 --- a/domain_access/src/Plugin/Action/DomainAccessEditAll.php +++ b/domain_access/src/Plugin/Action/DomainAccessEditAll.php @@ -2,8 +2,7 @@ namespace Drupal\domain_access\Plugin\Action; -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Session\AccountInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Assigns a user to all affiliates. @@ -14,26 +13,14 @@ * type = "user" * ) */ -class DomainAccessEditAll extends ActionBase { +class DomainAccessEditAll extends DomainAccessActionBase { /** * {@inheritdoc} */ public function execute($entity = NULL) { - $entity->set(DOMAIN_ACCESS_ALL_FIELD, 1); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, 1); $entity->save(); } - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - // @TODO: Check this logic. - /** @var \Drupal\user\UserInterface $object */ - $access = $object->access('update', $account, TRUE) - ->andIf($object->roles->access('edit', $account, TRUE)); - - return $return_as_object ? $access : $access->isAllowed(); - } - } diff --git a/domain_access/src/Plugin/Action/DomainAccessEditNone.php b/domain_access/src/Plugin/Action/DomainAccessEditNone.php index 5100fa7a..e914e3ba 100644 --- a/domain_access/src/Plugin/Action/DomainAccessEditNone.php +++ b/domain_access/src/Plugin/Action/DomainAccessEditNone.php @@ -2,8 +2,7 @@ namespace Drupal\domain_access\Plugin\Action; -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Session\AccountInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Removes a user from all affiliates. @@ -14,26 +13,14 @@ * type = "user" * ) */ -class DomainAccessEditNone extends ActionBase { +class DomainAccessEditNone extends DomainAccessActionBase { /** * {@inheritdoc} */ public function execute($entity = NULL) { - $entity->set(DOMAIN_ACCESS_ALL_FIELD, 0); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, 0); $entity->save(); } - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - // @TODO: Check this logic. - /** @var \Drupal\user\UserInterface $object */ - $access = $object->access('update', $account, TRUE) - ->andIf($object->roles->access('edit', $account, TRUE)); - - return $return_as_object ? $access : $access->isAllowed(); - } - } diff --git a/domain_access/src/Plugin/Action/DomainAccessNone.php b/domain_access/src/Plugin/Action/DomainAccessNone.php index 1d87a318..271e05a7 100644 --- a/domain_access/src/Plugin/Action/DomainAccessNone.php +++ b/domain_access/src/Plugin/Action/DomainAccessNone.php @@ -2,8 +2,7 @@ namespace Drupal\domain_access\Plugin\Action; -use Drupal\Core\Action\ActionBase; -use Drupal\Core\Session\AccountInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Removes a node to all affiliates.. @@ -14,26 +13,14 @@ * type = "node" * ) */ -class DomainAccessNone extends ActionBase { +class DomainAccessNone extends DomainAccessActionBase { /** * {@inheritdoc} */ public function execute($entity = NULL) { - $entity->set(DOMAIN_ACCESS_ALL_FIELD, 0); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD, 0); $entity->save(); } - /** - * {@inheritdoc} - */ - public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { - /** @var \Drupal\node\NodeInterface $object */ - // @TODO: Check this logic. - $result = $object->access('update', $account, TRUE) - ->andIf($object->status->access('edit', $account, TRUE)); - - return $return_as_object ? $result : $result->isAllowed(); - } - } diff --git a/domain_access/src/Plugin/Action/DomainAccessRemove.php b/domain_access/src/Plugin/Action/DomainAccessRemove.php index 4bbfe4a4..69d9a85c 100644 --- a/domain_access/src/Plugin/Action/DomainAccessRemove.php +++ b/domain_access/src/Plugin/Action/DomainAccessRemove.php @@ -2,6 +2,8 @@ namespace Drupal\domain_access\Plugin\Action; +use Drupal\domain_access\DomainAccessManagerInterface; + /** * Removes a node from a domain. * @@ -19,11 +21,11 @@ class DomainAccessRemove extends DomainAccessActionBase { public function execute($entity = NULL) { $id = $this->configuration['domain_id']; $node_domains = \Drupal::service('domain_access.manager')->getAccessValues($entity); - + // Remove domain assignment if present. if ($entity !== FALSE && isset($node_domains[$id])) { unset($node_domains[$id]); - $entity->set(DOMAIN_ACCESS_FIELD, array_keys($node_domains)); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, array_keys($node_domains)); $entity->save(); } } diff --git a/domain_access/src/Plugin/Action/DomainAccessRemoveEditor.php b/domain_access/src/Plugin/Action/DomainAccessRemoveEditor.php index 0c2e2790..d61943c4 100644 --- a/domain_access/src/Plugin/Action/DomainAccessRemoveEditor.php +++ b/domain_access/src/Plugin/Action/DomainAccessRemoveEditor.php @@ -2,6 +2,8 @@ namespace Drupal\domain_access\Plugin\Action; +use Drupal\domain_access\DomainAccessManagerInterface; + /** * Removes an editor from a domain. * @@ -23,7 +25,7 @@ public function execute($entity = NULL) { // Skip adding the role to the user if they already have it. if ($entity !== FALSE && isset($user_domains[$id])) { unset($user_domains[$id]); - $entity->set(DOMAIN_ACCESS_FIELD, array_keys($user_domains)); + $entity->set(DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, array_keys($user_domains)); $entity->save(); } } diff --git a/domain_access/src/Plugin/views/access/DomainAccessContent.php b/domain_access/src/Plugin/views/access/DomainAccessContent.php new file mode 100644 index 00000000..c58af3de --- /dev/null +++ b/domain_access/src/Plugin/views/access/DomainAccessContent.php @@ -0,0 +1,170 @@ +domainStorage = $domain_storage; + $this->userStorage = $user_storage; + $this->manager = $manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager')->getStorage('domain'), + $container->get('entity_type.manager')->getStorage('user'), + $container->get('domain_access.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function summaryTitle() { + return $this->t('Domain editor'); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account) { + // Users with this permission can see any domain content lists, and it is + // required to view all affiliates. + if ($account->hasPermission($this->allPermission)) { + return TRUE; + } + + // The routine below determines what domain (if any) was passed to the View. + if (isset($this->view->element['#arguments'])) { + foreach ($this->view->element['#arguments'] as $value) { + if ($domain = $this->domainStorage->load($value)) { + break; + } + } + } + + // Domain found, check user permissions. + if (!empty($domain)) { + return $this->manager->hasDomainPermissions($account, $domain, [$this->permission]); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function alterRouteDefinition(Route $route) { + if ($domains = $this->domainStorage->loadMultiple()) { + $list = array_keys($domains); + } + $list[] = 'all_affiliates'; + $route->setRequirement('_domain_access_views', (string) implode('+', $list)); + $route->setDefault('domain_permission', $this->permission); + $route->setDefault('domain_all_permission', $this->allPermission); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['user']; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return []; + } + +} diff --git a/domain_access/src/Plugin/views/access/DomainAccessEditor.php b/domain_access/src/Plugin/views/access/DomainAccessEditor.php new file mode 100644 index 00000000..2a34b2a8 --- /dev/null +++ b/domain_access/src/Plugin/views/access/DomainAccessEditor.php @@ -0,0 +1,30 @@ +load($this->argument)) { + if ($domain = \Drupal::entityTypeManager()->getStorage('domain')->load($this->argument)) { return $domain->label(); } return parent::title(); diff --git a/domain_access/src/Plugin/views/field/DomainAccessField.php b/domain_access/src/Plugin/views/field/DomainAccessField.php index 30456088..91fed373 100644 --- a/domain_access/src/Plugin/views/field/DomainAccessField.php +++ b/domain_access/src/Plugin/views/field/DomainAccessField.php @@ -3,14 +3,14 @@ namespace Drupal\domain_access\Plugin\views\field; use Drupal\views\ResultRow; -use Drupal\views\Plugin\views\field\Field; +use Drupal\views\Plugin\views\field\EntityField; /** * Field handler to present the link an entity on a domain. * * @ViewsField("domain_access_field") */ -class DomainAccessField extends Field { +class DomainAccessField extends EntityField { /** * {@inheritdoc} @@ -23,12 +23,13 @@ public function getItems(ResultRow $values) { foreach ($items as &$item) { $object = $item['raw']; $entity = $object->getEntity(); - $url = $entity->toUrl()->toString(); + // Mark the entity as external to force domain URL prefixing. + $url = $entity->toUrl('canonical', ['external' => TRUE])->toString(); $domain = $item['rendered']['#options']['entity']; $item['rendered']['#type'] = 'markup'; $item['rendered']['#markup'] = '' . $domain->label() . ''; } - uasort($items, array($this, 'sort')); + uasort($items, [$this, 'sort']); } return $items; @@ -41,10 +42,10 @@ private function sort($a, $b) { $domainA = isset($a['rendered']['#options']['entity']) ? $a['rendered']['#options']['entity'] : 0; $domainB = isset($b['rendered']['#options']['entity']) ? $b['rendered']['#options']['entity'] : 0; if ($domainA !== 0) { - return $domainA->getWeight() > $domainB->getWeight(); + return ($domainA->getWeight() > $domainB->getWeight()) ? 1 : 0; } // We don't have a domain object so sort as best we can. - return $a['rendered']['#plain_text'] > $b['rendered']['#plain_text']; + return strcmp($a['rendered']['#plain_text'], $b['rendered']['#plain_text']); } } diff --git a/domain_access/src/Plugin/views/filter/DomainAccessCurrentAllFilter.php b/domain_access/src/Plugin/views/filter/DomainAccessCurrentAllFilter.php index d23f6e2a..4010baa7 100644 --- a/domain_access/src/Plugin/views/filter/DomainAccessCurrentAllFilter.php +++ b/domain_access/src/Plugin/views/filter/DomainAccessCurrentAllFilter.php @@ -27,14 +27,14 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o * {@inheritdoc} */ public function getValueOptions() { - $this->valueOptions = array(1 => $this->t('Yes'), 0 => $this->t('No')); + $this->valueOptions = [1 => $this->t('Yes'), 0 => $this->t('No')]; } /** * {@inheritdoc} */ protected function operators() { - return array(); + return []; } /** @@ -42,17 +42,24 @@ protected function operators() { */ public function query() { $this->ensureMyTable(); - // @TODO: Proper abstraction of table and field name. - $all_table = $this->query->ensureTable('node__field_domain_all_affiliates'); - $current_domain = \Drupal::service('domain.negotiator')->getActiveId(); + $all_table = $this->query->addTable('node__field_domain_all_affiliates', $this->relationship); + $all_field = $all_table . '.field_domain_all_affiliates_value'; + $real_field = $this->tableAlias . '.' . $this->realField; + /** @var DomainNegotiatorInterface $domain_negotiator */ + $domain_negotiator = \Drupal::service('domain.negotiator'); + $current_domain = $domain_negotiator->getActiveDomain(); + $current_domain_id = $current_domain->id(); if (empty($this->value)) { - // @TODO proper handling of NULL? - $where = "$this->tableAlias.$this->realField <> '$current_domain'"; - $where = '(' . $where . " OR $this->tableAlias.$this->realField IS NULL)"; - $where = '(' . $where . " AND ($all_table.field_domain_all_affiliates_value = 0 OR $all_table.field_domain_all_affiliates_value IS NULL))"; + $where = "(($real_field <> '$current_domain_id' OR $real_field IS NULL) AND ($all_field = 0 OR $all_field IS NULL))"; + if ($current_domain->isDefault()) { + $where = "($real_field <> '$current_domain_id' AND ($all_field = 0 OR $all_field IS NULL))"; + } } else { - $where = "($this->tableAlias.$this->realField = '$current_domain' OR $all_table.field_domain_all_affiliates_value = 1)"; + $where = "($real_field = '$current_domain_id' OR $all_field = 1)"; + if ($current_domain->isDefault()) { + $where = "(($real_field = '$current_domain_id' OR $real_field IS NULL) OR $all_field = 1)"; + } } $this->query->addWhereExpression($this->options['group'], $where); // This filter causes duplicates. diff --git a/domain_access/src/Plugin/views/filter/DomainAccessFilter.php b/domain_access/src/Plugin/views/filter/DomainAccessFilter.php index cdfaec20..9952a00c 100644 --- a/domain_access/src/Plugin/views/filter/DomainAccessFilter.php +++ b/domain_access/src/Plugin/views/filter/DomainAccessFilter.php @@ -18,7 +18,7 @@ public function getValueOptions() { // @TODO: filter this list. if (!isset($this->valueOptions)) { $this->valueTitle = $this->t('Domains'); - $this->valueOptions = \Drupal::service('domain.loader')->loadOptionsList(); + $this->valueOptions = \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(); } return $this->valueOptions; } diff --git a/domain_access/src/Tests/DomainAccessPermissionsTest.php b/domain_access/src/Tests/DomainAccessPermissionsTest.php deleted file mode 100644 index 6952c6e8..00000000 --- a/domain_access/src/Tests/DomainAccessPermissionsTest.php +++ /dev/null @@ -1,271 +0,0 @@ -config('user.role.' . RoleInterface::AUTHENTICATED_ID)->set('permissions', array())->save(); - // Create Basic page node type. - if ($this->profile != 'standard') { - $this->drupalCreateContentType(array( - 'type' => 'page', - 'name' => 'Basic page', - 'display_submitted' => FALSE, - )); - } - $this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node'); - $this->manager = \Drupal::service('domain_access.manager'); - // Create 5 domains. - $this->domainCreateTestDomains(5); - } - - /** - * Runs basic tests for node_access function. - */ - public function testDomainAccessPermissions() { - // Note that these are hook_node_access() rules. Node Access system tests - // are in DomainAccessRecordsTest. - // We expect to find 5 domain options. Set two for later use. - $domains = \Drupal::service('domain.loader')->loadMultiple(); - foreach ($domains as $domain) { - if (!isset($one)) { - $one = $domain->id(); - continue; - } - if (!isset($two)) { - $two = $domain->id(); - } - } - - $user_storage = \Drupal::entityTypeManager()->getStorage('user'); - - // Assign our user to domain $two. Test on $one and $two. - $domain_user1 = $this->drupalCreateUser(array( - 'access content', - 'edit domain content', - 'delete domain content', - )); - $this->addDomainToEntity('user', $domain_user1->id(), $two); - $domain_user1 = $user_storage->load($domain_user1->id()); - $assigned = $this->manager->getAccessValues($domain_user1); - $this->assertTrue(count($assigned) == 1, 'User assigned to one domain.'); - $this->assertTrue(isset($assigned[$two]), 'User assigned to proper test domain.'); - - // Assign one node to default domain, and one to our test domain. - $domain_node1 = $this->drupalCreateNode(array('type' => 'page', DOMAIN_ACCESS_FIELD => [$one])); - $domain_node2 = $this->drupalCreateNode(array('type' => 'page', DOMAIN_ACCESS_FIELD => [$two])); - $assigned = $this->manager->getAccessValues($domain_node1); - $this->assertTrue(isset($assigned[$one]), 'Node1 assigned to proper test domain.'); - $assigned = $this->manager->getAccessValues($domain_node2); - $this->assertTrue(isset($assigned[$two]), 'Node2 assigned to proper test domain.'); - - // Tests 'edit domain content' to edit content assigned to their domains. - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => FALSE, - 'delete' => FALSE, - ), $domain_node1, $domain_user1); - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => TRUE, - 'delete' => TRUE, - ), $domain_node2, $domain_user1); - - // Tests 'edit domain TYPE content'. - // Assign our user to domain $two. Test on $one and $two. - $domain_user3 = $this->drupalCreateUser(array( - 'access content', - 'update page content on assigned domains', - 'delete page content on assigned domains', - )); - $this->addDomainToEntity('user', $domain_user3->id(), $two); - $domain_user3 = $user_storage->load($domain_user3->id()); - $assigned = $this->manager->getAccessValues($domain_user3); - $this->assertTrue(count($assigned) == 1, 'User assigned to one domain.'); - $this->assertTrue(isset($assigned[$two]), 'User assigned to proper test domain.'); - - // Assign two different node types to our test domain. - $domain_node3 = $this->drupalCreateNode(array('type' => 'article', DOMAIN_ACCESS_FIELD => [$two])); - $domain_node4 = $this->drupalCreateNode(array('type' => 'page', DOMAIN_ACCESS_FIELD => [$two])); - $assigned = $this->manager->getAccessValues($domain_node3); - $this->assertTrue(isset($assigned[$two]), 'Node3 assigned to proper test domain.'); - $assigned = $this->manager->getAccessValues($domain_node4); - $this->assertTrue(isset($assigned[$two]), 'Node4 assigned to proper test domain.'); - - // Tests 'edit TYPE content on assigned domains'. - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => FALSE, - 'delete' => FALSE, - ), $domain_node3, $domain_user3); - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => TRUE, - 'delete' => TRUE, - ), $domain_node4, $domain_user3); - - // @TODO: Test edit and delete for user with 'all affiliates' permission. - // Tests 'edit domain TYPE content'. - // Assign our user to domain $two. Test on $one and $two. - $domain_user4 = $this->drupalCreateUser(array( - 'access content', - 'update page content on assigned domains', - 'delete page content on assigned domains', - )); - $this->addDomainToEntity('user', $domain_user4->id(), $two); - $this->addDomainToEntity('user', $domain_user4->id(), 1, DOMAIN_ACCESS_ALL_FIELD); - $domain_user4 = $user_storage->load($domain_user4->id()); - $assigned = $this->manager->getAccessValues($domain_user4); - $this->assertTrue(count($assigned) == 1, 'User assigned to one domain.'); - $this->assertTrue(isset($assigned[$two]), 'User assigned to proper test domain.'); - $this->assertTrue(!empty($domain_user4->get(DOMAIN_ACCESS_ALL_FIELD)->value), 'User assign to all affiliates.'); - - // Assign two different node types to our test domain. - $domain_node5 = $this->drupalCreateNode(array('type' => 'article', DOMAIN_ACCESS_FIELD => [$one])); - $domain_node6 = $this->drupalCreateNode(array('type' => 'page', DOMAIN_ACCESS_FIELD => [$one])); - $assigned = $this->manager->getAccessValues($domain_node5); - $this->assertTrue(isset($assigned[$one]), 'Node5 assigned to proper test domain.'); - $assigned = $this->manager->getAccessValues($domain_node6); - $this->assertTrue(isset($assigned[$one]), 'Node6 assigned to proper test domain.'); - - // Tests 'edit TYPE content on assigned domains'. - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => FALSE, - 'delete' => FALSE, - ), $domain_node5, $domain_user4); - $this->assertNodeAccess(array( - 'view' => TRUE, - 'update' => TRUE, - 'delete' => TRUE, - ), $domain_node6, $domain_user4); - - // Tests create permissions. Any content on assigned domains. - $domain_user5 = $this->drupalCreateUser(array('access content', 'create domain content')); - $this->addDomainToEntity('user', $domain_user5->id(), $two); - $domain_user5 = $user_storage->load($domain_user5->id()); - $assigned = $this->manager->getAccessValues($domain_user5); - $this->assertTrue(count($assigned) == 1, 'User assigned to one domain.'); - $this->assertTrue(isset($assigned[$two]), 'User assigned to proper test domain.'); - // This test is domain sensitive. - foreach ($domains as $domain) { - $this->domainLogin($domain, $domain_user5); - $url = $domain->getPath() . 'node/add/page'; - $this->drupalGet($url); - if ($domain->id() == $two) { - $this->assertResponse(200); - } - else { - $this->assertResponse(403); - } - } - // Tests create permissions. Page content on assigned domains. - $domain_user5 = $this->drupalCreateUser(array('access content', 'create page content on assigned domains')); - $this->addDomainToEntity('user', $domain_user5->id(), $two); - $domain_user5 = $user_storage->load($domain_user5->id()); - $assigned = $this->manager->getAccessValues($domain_user5); - $this->assertTrue(count($assigned) == 1, 'User assigned to one domain.'); - $this->assertTrue(isset($assigned[$two]), 'User assigned to proper test domain.'); - // This test is domain sensitive. - foreach ($domains as $domain) { - $this->domainLogin($domain, $domain_user5); - $url = $domain->getPath() . 'node/add/page'; - $this->drupalGet($url); - if ($domain->id() == $two) { - $this->assertResponse(200); - } - else { - $this->assertResponse(403); - } - $url = $domain->getPath() . 'node/add/article'; - $this->drupalGet($url); - $this->assertResponse(403); - } - - } - - /** - * Asserts that node access correctly grants or denies access. - * - * @param array $ops - * An associative array of the expected node access grants for the node - * and account, with each key as the name of an operation (e.g. 'view', - * 'delete') and each value a Boolean indicating whether access to that - * operation should be granted. - * @param \Drupal\node\Entity\Node $node - * The node object to check. - * @param \Drupal\Core\Session\AccountInterface $account - * The user account for which to check access. - */ - public function assertNodeAccess(array $ops, Node $node, AccountInterface $account) { - foreach ($ops as $op => $result) { - $this->assertEqual($result, $this->accessHandler->access($node, $op, $account), $this->nodeAccessAssertMessage($op, $result)); - } - } - - /** - * Constructs an assert message to display which node access was tested. - * - * @param string $operation - * The operation to check access for. - * @param bool $result - * Whether access should be granted or not. - * @param string|null $langcode - * (optional) The language code indicating which translation of the node - * to check. If NULL, the untranslated (fallback) access is checked. - * - * @return string - * An assert message string which contains information in plain English - * about the node access permission test that was performed. - */ - public function nodeAccessAssertMessage($operation, $result, $langcode = NULL) { - return new FormattableMarkup( - 'Node access returns @result with operation %op, language code %langcode.', - array( - '@result' => $result ? 'true' : 'false', - '%op' => $operation, - '%langcode' => !empty($langcode) ? $langcode : 'empty', - ) - ); - } - -} diff --git a/domain_access/tests/modules/domain_access_test/domain_access_test.info.yml b/domain_access/tests/modules/domain_access_test/domain_access_test.info.yml index f809a01a..3063d286 100644 --- a/domain_access/tests/modules/domain_access_test/domain_access_test.info.yml +++ b/domain_access/tests/modules/domain_access_test/domain_access_test.info.yml @@ -2,11 +2,16 @@ name: "Domain Access module tests" description: "Support module for domain access testing." type: module package: Testing -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 hidden: TRUE dependencies: - domain - domain_access - taxonomy + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_access/src/Tests/DomainAccessAllAffiliatesTest.php b/domain_access/tests/src/Functional/DomainAccessAllAffiliatesTest.php similarity index 76% rename from domain_access/src/Tests/DomainAccessAllAffiliatesTest.php rename to domain_access/tests/src/Functional/DomainAccessAllAffiliatesTest.php index e427e2c7..c0c359bf 100644 --- a/domain_access/src/Tests/DomainAccessAllAffiliatesTest.php +++ b/domain_access/tests/src/Functional/DomainAccessAllAffiliatesTest.php @@ -1,10 +1,9 @@ admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'administer content types', 'administer node fields', 'administer node display', 'administer domains', - )); + ]); $this->drupalLogin($this->admin_user); // Visit the article field administration page. @@ -53,14 +52,14 @@ public function testDomainAccessAllField() { */ public function testDomainAccessAllFieldStorage() { $label = 'Send to all affiliates'; - $this->admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', 'administer node display', 'administer domains', 'publish to any domain', - )); + ]); $this->drupalLogin($this->admin_user); // Create 5 domains. @@ -74,10 +73,10 @@ public function testDomainAccessAllFieldStorage() { $this->assertText($label, 'Found the domain field instance.'); // We expect to find 5 domain options. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $string = 'value="' . $domain->id() . '"'; - $this->assertRaw($string, new FormattableMarkup('Found the %domain option.', array('%domain' => $domain->label()))); + $this->assertRaw($string, 'Found the domain option.'); if (!isset($one)) { $one = $domain->id(); continue; @@ -92,14 +91,15 @@ public function testDomainAccessAllFieldStorage() { $edit["field_domain_access[{$one}]"] = TRUE; $edit["field_domain_access[{$two}]"] = TRUE; $edit["field_domain_all_affiliates[value]"] = 1; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertResponse(200); $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); // Check that two values are set. $values = \Drupal::service('domain_access.manager')->getAccessValues($node); - $this->assertTrue(count($values) == 2, 'Node saved with two domain records.'); + $this->assertCount(2, $values, 'Node saved with two domain records.'); // Check that all affiliates is set. - $this->assertTrue(!empty($node->get(DOMAIN_ACCESS_ALL_FIELD)->value), 'Node assigned to all affiliates.'); + $this->assertNotEmpty($node->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value, 'Node assigned to all affiliates.'); } } diff --git a/domain_access/tests/src/Functional/DomainAccessCacheTest.php b/domain_access/tests/src/Functional/DomainAccessCacheTest.php new file mode 100644 index 00000000..31ba4280 --- /dev/null +++ b/domain_access/tests/src/Functional/DomainAccessCacheTest.php @@ -0,0 +1,79 @@ +domainTableIsEmpty(); + + // Create a new domain programmatically. + $this->domainCreateTestDomains(5); + $expected = []; + + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath()); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . ':'; + } + + $database = \Drupal::database(); + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), 'Cache returns as expected.'); + + // Now create a node and test the cache. + // Create an article node assigned to two domains. + $ids = ['example_com', 'four_example_com']; + $node1 = $this->drupalCreateNode([ + 'type' => 'article', + 'field_domain_access' => [$ids], + 'path' => '/test' + ]); + + $original = $expected; + + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath() . 'test'); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . 'test:'; + } + + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), 'Cache returns as expected.'); + + // When we delete the node, we want all cids removed. + $node1->delete(); + + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($original), sort($result), 'Cache returns as expected.'); + + } + +} diff --git a/domain_access/tests/src/Functional/DomainAccessContentUrlsTest.php b/domain_access/tests/src/Functional/DomainAccessContentUrlsTest.php new file mode 100644 index 00000000..25a7a14e --- /dev/null +++ b/domain_access/tests/src/Functional/DomainAccessContentUrlsTest.php @@ -0,0 +1,85 @@ + 'page', + 'title' => 'foo', + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [ + 'example_com', + 'one_example_com', + 'two_example_com', + ], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 0, + ]; + $node = $this->createNode($nodes_values); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $expected = base_path() . $path; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertEquals($expected, $url, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUri'); + + // Get the path processor service. + $paths = \Drupal::service('domain_access.manager'); + $urls = $paths->getContentUrls($node); + $expected = [ + 'example_com' => $domains['example_com']->getPath() . 'node/1', + $id => $domains[$id]->getPath() . 'node/1', + 'two_example_com' => $domains['two_example_com']->getPath() . 'node/1', + ]; + $this->assertEquals($expected, $urls); + } + +} diff --git a/domain_access/src/Tests/DomainAccessDefaultValueTest.php b/domain_access/tests/src/Functional/DomainAccessDefaultValueTest.php similarity index 73% rename from domain_access/src/Tests/DomainAccessDefaultValueTest.php rename to domain_access/tests/src/Functional/DomainAccessDefaultValueTest.php index bf366851..9c7732eb 100644 --- a/domain_access/src/Tests/DomainAccessDefaultValueTest.php +++ b/domain_access/tests/src/Functional/DomainAccessDefaultValueTest.php @@ -1,10 +1,8 @@ admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', 'administer node display', 'administer domains', 'publish to any domain', - )); + ]); $this->drupalLogin($this->admin_user); // Create 5 domains. @@ -55,25 +53,26 @@ public function testDomainAccessDefaultValue() { 'title[0][value]' => 'Test node', 'field_domain_access[example_com]' => 'example_com', ]; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); // Load the node. $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); - $this->assertTrue($node, 'Article node created.'); + $this->assertNotNull($node, 'Article node created.'); // Check that the values are set. $values = \Drupal::service('domain_access.manager')->getAccessValues($node); - $this->assertTrue(count($values) == 1, 'Node saved with one domain record.'); + $this->assertCount(1, $values, 'Node saved with one domain record.'); $allValue = \Drupal::service('domain_access.manager')->getAllValue($node); - $this->assertTrue(empty($allValue), 'Not sent to all affiliates.'); + $this->assertEmpty($allValue, 'Not sent to all affiliates.'); // Logout the admin user. $this->drupalLogout(); // Create a limited value user. - $this->test_user = $this->drupalCreateUser(array( + $this->test_user = $this->drupalCreateUser([ 'create article content', 'edit any article content', - )); + ]); // Login and try to edit the created node. $this->drupalLogin($this->test_user); @@ -85,16 +84,17 @@ public function testDomainAccessDefaultValue() { $edit = [ 'title[0][value]' => 'Test node update', ]; - $this->drupalPostForm('node/1/edit', $edit, 'Save'); + $this->drupalGet('node/1/edit'); + $this->submitForm($edit, 'Save'); // Load the node. $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); - $this->assertTrue($node, 'Article node created.'); + $this->assertNotNull($node, 'Article node created.'); // Check that the values are set. $values = \Drupal::service('domain_access.manager')->getAccessValues($node); - $this->assertTrue(count($values) == 1, 'Node saved with one domain record.'); + $this->assertCount(1, $values, 'Node saved with one domain record.'); $allValue = \Drupal::service('domain_access.manager')->getAllValue($node); - $this->assertTrue(empty($allValue), 'Not sent to all affiliates.'); + $this->assertEmpty($allValue, 'Not sent to all affiliates.'); } diff --git a/domain_access/tests/src/Functional/DomainAccessElementTest.php b/domain_access/tests/src/Functional/DomainAccessElementTest.php index fe70bfad..66a9fed7 100644 --- a/domain_access/tests/src/Functional/DomainAccessElementTest.php +++ b/domain_access/tests/src/Functional/DomainAccessElementTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\domain_access\Functional; use Drupal\Tests\domain\Functional\DomainTestBase; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Tests behavior for the domain access field element. @@ -16,7 +17,13 @@ class DomainAccessElementTest extends DomainTestBase { * * @var array */ - public static $modules = array('domain', 'domain_access', 'field', 'field_ui', 'user'); + public static $modules = [ + 'domain', + 'domain_access', + 'field', + 'field_ui', + 'user', + ]; /** * {@inheritdoc} @@ -24,48 +31,52 @@ class DomainAccessElementTest extends DomainTestBase { protected function setUp() { parent::setUp(); - // Run the install hook. - // @TODO: figure out why this is necessary. - module_load_install('domain_access'); - domain_access_install(); - // Create 5 domains. $this->domainCreateTestDomains(5); } /** - * Basic test setup. + * Test runner. */ public function testDomainAccessElement() { - $admin = $this->drupalCreateUser(array( + $this->runInstalledTest('article'); + $node_type = $this->createContentType(['type' => 'test']); + $this->runInstalledTest('test'); + } + + /** + * Basic test setup. + */ + public function runInstalledTest($node_type) { + $admin = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', 'administer node display', 'administer domains', 'publish to any domain', - )); + ]); $this->drupalLogin($admin); - $this->drupalGet('node/add/article'); + $this->drupalGet('node/add/' . $node_type); $this->assertSession()->statusCodeEquals(200); // Set the title, so the node can be saved. $this->fillField('title[0][value]', 'Test node'); // We expect to find 5 domain options. We set two as selected. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); $count = 0; $ids = ['example_com', 'one_example_com', 'two_example_com']; foreach ($domains as $domain) { - $locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if (in_array($domain->id(), $ids)) { $this->checkField($locator); } } // Find the all affiliates field. - $locator = DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; $this->findField($locator); // Set all affiliates to TRUE. @@ -75,8 +86,10 @@ public function testDomainAccessElement() { $this->pressButton('edit-submit'); $this->assertSession()->statusCodeEquals(200); + // Get node data. Note that we create one new node for each test case. $storage = \Drupal::entityTypeManager()->getStorage('node'); - $node = $storage->load(1); + $nid = $node_type == 'article' ? 1 : 2; + $node = $storage->load($nid); // Check that two values are set. $manager = \Drupal::service('domain_access.manager'); $values = $manager->getAccessValues($node); @@ -85,9 +98,13 @@ public function testDomainAccessElement() { $this->assert($value == 1, 'Node saved to all affiliates.'); // Now login as a user with limited rights. - $account = $this->drupalCreateUser(array('create article content', 'edit any article content', 'publish to any assigned domain')); + $account = $this->drupalCreateUser([ + 'create ' . $node_type . ' content', + 'edit any ' . $node_type . ' content', + 'publish to any assigned domain', + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ACCESS_FIELD); + $this->addDomainsToEntity('user', $account->id(), $ids, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); $user_storage = \Drupal::entityTypeManager()->getStorage('user'); $user = $user_storage->load($account->id()); $values = $manager->getAccessValues($user); @@ -101,7 +118,7 @@ public function testDomainAccessElement() { $this->assertSession()->statusCodeEquals(200); foreach ($domains as $domain) { - $locator = DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; $this->findField($locator); if ($domain->id() == 'example_com') { $this->checkField($locator); @@ -115,7 +132,7 @@ public function testDomainAccessElement() { } } - $locator = DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; $this->assertSession()->fieldNotExists($locator); // Save the form. @@ -123,8 +140,8 @@ public function testDomainAccessElement() { $this->assertSession()->statusCodeEquals(200); // Now, check the node. - $storage->resetCache(array($node->id())); - $node = $storage->load(1); + $storage->resetCache([$node->id()]); + $node = $storage->load($node->id()); // Check that two values are set. $values = $manager->getAccessValues($node); $this->assert(count($values) == 2, 'Node saved with two domain records.'); diff --git a/domain_access/src/Tests/DomainAccessEntityFieldTest.php b/domain_access/tests/src/Functional/DomainAccessEntityFieldTest.php similarity index 74% rename from domain_access/src/Tests/DomainAccessEntityFieldTest.php rename to domain_access/tests/src/Functional/DomainAccessEntityFieldTest.php index 063875f6..48d1b922 100644 --- a/domain_access/src/Tests/DomainAccessEntityFieldTest.php +++ b/domain_access/tests/src/Functional/DomainAccessEntityFieldTest.php @@ -1,12 +1,10 @@ domainCreateTestDomains(5); } @@ -43,13 +44,13 @@ protected function setUp() { public function testDomainAccessEntityFields() { $label = 'Send to all affiliates'; // Create a vocabulary. - $vocabulary = entity_create('taxonomy_vocabulary', array( + $vocabulary = Vocabulary::create([ 'name' => 'Domain vocabulary', 'description' => 'Test taxonomy for Domain Access', 'vid' => 'domain_access', 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, 'weight' => 100, - )); + ]); $vocabulary->save(); $text['taxonomy_term'] = [ 'name' => 'term', @@ -57,7 +58,7 @@ public function testDomainAccessEntityFields() { 'description' => 'Make this term available on all domains.', ]; domain_access_confirm_fields('taxonomy_term', 'domain_access', $text); - $this->admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', @@ -67,7 +68,7 @@ public function testDomainAccessEntityFields() { 'administer taxonomy', 'administer taxonomy_term fields', 'administer taxonomy_term form display', - )); + ]); $this->drupalLogin($this->admin_user); $this->drupalGet('admin/structure/taxonomy/manage/domain_access/overview/fields'); $this->assertResponse(200, 'Manage fields page accessed.'); diff --git a/domain_access/src/Tests/DomainAccessEntityReferenceTest.php b/domain_access/tests/src/Functional/DomainAccessEntityReferenceTest.php similarity index 78% rename from domain_access/src/Tests/DomainAccessEntityReferenceTest.php rename to domain_access/tests/src/Functional/DomainAccessEntityReferenceTest.php index ecc3aa99..428192d9 100644 --- a/domain_access/src/Tests/DomainAccessEntityReferenceTest.php +++ b/domain_access/tests/src/Functional/DomainAccessEntityReferenceTest.php @@ -1,10 +1,8 @@ admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'administer content types', 'administer node fields', 'administer node display', 'administer domains', - )); + ]); $this->drupalLogin($this->admin_user); // Visit the article field administration page. @@ -51,14 +49,14 @@ public function testDomainAccessNodeField() { * Tests the storage of the domain access field. */ public function testDomainAccessFieldStorage() { - $this->admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', 'administer node display', 'administer domains', 'publish to any domain', - )); + ]); $this->drupalLogin($this->admin_user); // Create 5 domains. @@ -72,10 +70,10 @@ public function testDomainAccessFieldStorage() { $this->assertText('Domain Access', 'Found the domain field instance.'); // We expect to find 5 domain options. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $string = 'value="' . $domain->id() . '"'; - $this->assertRaw($string, new FormattableMarkup('Found the %domain option.', array('%domain' => $domain->label()))); + $this->assertRaw($string, 'Found the domain option.'); if (!isset($one)) { $one = $domain->id(); continue; @@ -89,12 +87,13 @@ public function testDomainAccessFieldStorage() { $edit['title[0][value]'] = 'Test node'; $edit["field_domain_access[{$one}]"] = TRUE; $edit["field_domain_access[{$two}]"] = TRUE; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertResponse(200); $node = \Drupal::entityTypeManager()->getStorage('node')->load(1); // Check that two values are set. $values = \Drupal::service('domain_access.manager')->getAccessValues($node); - $this->assertTrue(count($values) == 2, 'Node saved with two domain records.'); + $this->assertCount(2, $values, 'Node saved with two domain records.'); } } diff --git a/domain_access/src/Tests/DomainAccessFieldTest.php b/domain_access/tests/src/Functional/DomainAccessFieldTest.php similarity index 69% rename from domain_access/src/Tests/DomainAccessFieldTest.php rename to domain_access/tests/src/Functional/DomainAccessFieldTest.php index 40ef9ebe..3e970536 100644 --- a/domain_access/src/Tests/DomainAccessFieldTest.php +++ b/domain_access/tests/src/Functional/DomainAccessFieldTest.php @@ -1,12 +1,10 @@ drupalCreateUser(array('create article content', 'publish to any domain')); + $user1 = $this->drupalCreateUser(['create article content', 'publish to any domain']); $this->drupalLogin($user1); // Visit the article creation page. @@ -47,16 +51,16 @@ public function testDomainAccessFields() { $this->assertResponse(200, 'Article creation found.'); // Check for the form options. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $this->assertText($domain->label(), 'Domain form item found.'); } $this->assertText($label, 'All affiliates field found.'); // Test a user who can access some domain settings. - $user2 = $this->drupalCreateUser(array('create article content', 'publish to any assigned domain')); + $user2 = $this->drupalCreateUser(['create article content', 'publish to any assigned domain']); $active_domain = array_rand($domains, 1); - $this->addDomainToEntity('user', $user2->id(), $active_domain); + $this->addDomainsToEntity('user', $user2->id(), $active_domain, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); $this->drupalLogin($user2); // Visit the article creation page. @@ -66,16 +70,16 @@ public function testDomainAccessFields() { // Check for the form options. foreach ($domains as $domain) { if ($domain->id() == $active_domain) { - $this->assertText($domain->label(), 'Domain form item found.'); + $this->assertRaw('>' . $domain->label() . '', 'Domain form item found.'); } else { - $this->assertNoText($domain->label(), 'Domain form item not found.'); + $this->assertNoRaw('>' . $domain->label() . '', 'Domain form item not found.'); } } $this->assertNoText($label, 'All affiliates field not found.'); // Test a user who can access no domain settings. - $user3 = $this->drupalCreateUser(array('create article content')); + $user3 = $this->drupalCreateUser(['create article content']); $this->drupalLogin($user3); // Visit the article creation page. @@ -92,17 +96,18 @@ public function testDomainAccessFields() { // The domain/domain affiliates fields are not accessible to this user. // The save will fail with an EntityStorageException until // https://www.drupal.org/node/2609252 is fixed. - $edit = array(); + $edit = []; $edit['title[0][value]'] = $this->randomMachineName(8); $edit['body[0][value]'] = $this->randomMachineName(16); - $this->drupalPostForm('node/add/article', $edit, t('Save')); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); // Check that the node exists in the database. $node = $this->drupalGetNodeByTitle($edit['title[0][value]']); - $this->assertTrue($node, 'Node found in database.'); + $this->assertNotNull($node, 'Node found in database.'); // Test a user who can assign users to domains. - $user4 = $this->drupalCreateUser(array('administer users', 'assign editors to any domain')); + $user4 = $this->drupalCreateUser(['administer users', 'assign editors to any domain']); $this->drupalLogin($user4); // Visit the account creation page. @@ -115,9 +120,9 @@ public function testDomainAccessFields() { } // Test a user who can assign users to some domains. - $user5 = $this->drupalCreateUser(array('administer users', 'assign domain editors')); + $user5 = $this->drupalCreateUser(['administer users', 'assign domain editors']); $active_domain = array_rand($domains, 1); - $this->addDomainToEntity('user', $user5->id(), $active_domain); + $this->addDomainsToEntity('user', $user5->id(), $active_domain, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); $this->drupalLogin($user5); // Visit the account creation page. @@ -127,15 +132,15 @@ public function testDomainAccessFields() { // Check for the form options. foreach ($domains as $domain) { if ($domain->id() == $active_domain) { - $this->assertText($domain->label(), 'Domain form item found.'); + $this->assertRaw('>' . $domain->label() . '', 'Domain form item found.'); } else { - $this->assertNoText($domain->label(), 'Domain form item not found.'); + $this->assertNoRaw('>' . $domain->label() . '', 'Domain form item not found.'); } } // Test a user who can access no domain settings. - $user6 = $this->drupalCreateUser(array('administer users')); + $user6 = $this->drupalCreateUser(['administer users']); $this->drupalLogin($user6); // Visit the account creation page. @@ -148,35 +153,37 @@ public function testDomainAccessFields() { } // Test a user who can access all domain settings. - $user7 = $this->drupalCreateUser(array('bypass node access', 'publish to any domain')); + $user7 = $this->drupalCreateUser(['bypass node access', 'publish to any domain']); $this->drupalLogin($user7); // Create a new content type and test that the fields are created. // Create a content type programmatically. $type = $this->drupalCreateContentType(); - $type_exists = (bool) NodeType::load($type->id()); $this->assertTrue($type_exists, 'The new content type has been created in the database.'); + // The test is not passing to domain_access_node_type_insert() properly. + domain_access_confirm_fields('node', $type->id()); + // Visit the article creation page. $this->drupalGet('node/add/' . $type->id()); $this->assertResponse(200, $type->id() . ' creation found.'); // Check for the form options. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $this->assertText($domain->label(), 'Domain form item found.'); } $this->assertText($label, 'All affiliates field found.'); // Test user without access to affiliates field editing their user page. - $user8 = $this->drupalCreateUser(array('change own username')); + $user8 = $this->drupalCreateUser(['change own username']); $this->drupalLogin($user8); $user_edit_page = 'user/' . $user8->id() . '/edit'; $this->drupalGet($user_edit_page); // Check for the form options. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $this->assertNoText($domain->label(), 'Domain form item not found.'); } @@ -184,10 +191,11 @@ public function testDomainAccessFields() { $this->assertNoText($label, 'All affiliates field not found.'); // Change own username. - $edit = array(); + $edit = []; $edit['name'] = $this->randomMachineName(); - $this->drupalPostForm($user_edit_page, $edit, t('Save')); + $this->drupalGet($user_edit_page); + $this->submitForm($edit, 'Save'); } } diff --git a/domain_access/src/Tests/DomainAccessGrantsTest.php b/domain_access/tests/src/Functional/DomainAccessGrantsTest.php similarity index 70% rename from domain_access/src/Tests/DomainAccessGrantsTest.php rename to domain_access/tests/src/Functional/DomainAccessGrantsTest.php index e24c74d9..43078e98 100644 --- a/domain_access/src/Tests/DomainAccessGrantsTest.php +++ b/domain_access/tests/src/Functional/DomainAccessGrantsTest.php @@ -1,9 +1,10 @@ domainCreateTestDomains(5); // Assign a node to a random domain. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); $active_domain = array_rand($domains, 1); $domain = $domains[$active_domain]; // Create an article node. - $node1 = $this->drupalCreateNode(array( + $node1 = $this->drupalCreateNode([ 'type' => 'article', - DOMAIN_ACCESS_FIELD => array($domain->id()), - )); - $this->assertTrue($node_storage->load($node1->id()), 'Article node created.'); + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$domain->id()], + ]); + $this->assertNotNull($node_storage->load($node1->id()), 'Article node created.'); // Test the response of the node on each site. Should allow access only to // the selected site. @@ -70,12 +71,12 @@ public function testDomainAccessGrants() { } // Create an article node. - $node2 = $this->drupalCreateNode(array( + $node2 = $this->drupalCreateNode([ 'type' => 'article', - DOMAIN_ACCESS_FIELD => array($domain->id()), - DOMAIN_ACCESS_ALL_FIELD => 1, - )); - $this->assertTrue($node_storage->load($node2->id()), 'Article node created.'); + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$domain->id()], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 1, + ]); + $this->assertNotNull($node_storage->load($node2->id()), 'Article node created.'); // Test the response of the node on each site. Should allow access on all. foreach ($domains as $domain) { $path = $domain->getPath() . 'node/' . $node2->id(); diff --git a/domain_access/tests/src/Functional/DomainAccessLanguageSaveTest.php b/domain_access/tests/src/Functional/DomainAccessLanguageSaveTest.php new file mode 100644 index 00000000..60655d75 --- /dev/null +++ b/domain_access/tests/src/Functional/DomainAccessLanguageSaveTest.php @@ -0,0 +1,90 @@ +domainCreateTestDomains(5); + + // Add Hungarian and Afrikaans. + ConfigurableLanguage::createFromLangcode('hu')->save(); + ConfigurableLanguage::createFromLangcode('af')->save(); + + // Enable content translation for the current entity type. + \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); + } + + /** + * Basic test setup. + */ + public function testDomainAccessSave() { + $storage = \Drupal::entityTypeManager()->getStorage('node'); + // Save a node programmatically. + $node = $storage->create([ + 'type' => 'article', + 'title' => 'Test node', + 'uid' => '1', + 'status' => 1, + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => ['example_com'], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 1, + ]); + $node->save(); + + // Load the node. + $node = $storage->load(1); + + // Check that two values are set properly. + $manager = \Drupal::service('domain_access.manager'); + $values = $manager->getAccessValues($node); + $this->assert(count($values) == 1, 'Node saved with one domain records.'); + $value = $manager->getAllValue($node); + $this->assert($value == 1, 'Node saved to all affiliates.'); + + // Create an Afrikaans translation assigned to domain 2. + $translation = $node->addTranslation('af'); + $translation->title->value = $this->randomString(); + $translation->{DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD} = ['example_com', 'one_example_com']; + $translation->{DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD} = 0; + $translation->status = 1; + $node->save(); + + // Load and check the translated node. + $parent_node = $storage->load(1); + $node = $parent_node->getTranslation('af'); + $values = $manager->getAccessValues($node); + $this->assert(count($values) == 2, 'Node saved with two domain records.'); + $value = $manager->getAllValue($node); + $this->assert($value == 0, 'Node not saved to all affiliates.'); + } + +} diff --git a/domain_access/tests/src/Functional/DomainAccessPermissionsTest.php b/domain_access/tests/src/Functional/DomainAccessPermissionsTest.php new file mode 100644 index 00000000..98f15a59 --- /dev/null +++ b/domain_access/tests/src/Functional/DomainAccessPermissionsTest.php @@ -0,0 +1,301 @@ +config('user.role.' . RoleInterface::AUTHENTICATED_ID)->set('permissions', [])->save(); + // Create Basic page node type. + if ($this->profile != 'standard') { + $this->drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Basic page', + 'display_submitted' => FALSE, + ]); + $this->drupalCreateContentType([ + 'type' => 'article', + 'name' => 'Article', + 'display_submitted' => FALSE, + ]); + } + $this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node'); + $this->manager = \Drupal::service('domain_access.manager'); + $this->userStorage = \Drupal::entityTypeManager()->getStorage('user'); + // Create 5 domains. + $this->domainCreateTestDomains(5); + $this->domains = $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + } + + /** + * Runs basic tests for node_access function. + */ + public function testDomainAccessPermissions() { + // Note that these are hook_node_access() rules. Node Access system tests + // are in DomainAccessRecordsTest. + // We expect to find 5 domain options. Set two for later use. + foreach ($this->domains as $domain) { + if (!isset($one)) { + $one = $domain->id(); + continue; + } + if (!isset($two)) { + $two = $domain->id(); + } + } + + // Assign our user to domain $two. Test on $one and $two. + $domain_user1 = $this->drupalCreateUser([ + 'access content', + 'edit domain content', + 'delete domain content', + ]); + $this->addDomainsToEntity('user', $domain_user1->id(), $two, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $domain_user1 = $this->userStorage->load($domain_user1->id()); + $assigned = $this->manager->getAccessValues($domain_user1); + $this->assertCount(1, $assigned, 'User assigned to one domain.'); + $this->assertArrayHasKey($two, $assigned, 'User assigned to proper test domain.'); + // Assign one node to default domain, and one to our test domain. + $domain_node1 = $this->drupalCreateNode(['type' => 'page', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$one]]); + $domain_node2 = $this->drupalCreateNode(['type' => 'page', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$two]]); + $assigned = $this->manager->getAccessValues($domain_node1); + $this->assertArrayHasKey($one, $assigned, 'Node1 assigned to proper test domain.'); + $assigned = $this->manager->getAccessValues($domain_node2); + $this->assertArrayHasKey($two, $assigned, 'Node2 assigned to proper test domain.'); + + // Tests 'edit domain content' to edit content assigned to their domains. + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => FALSE, + 'delete' => FALSE, + ], $domain_node1, $domain_user1); + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => TRUE, + 'delete' => TRUE, + ], $domain_node2, $domain_user1); + + // Tests 'edit domain TYPE content'. + // Assign our user to domain $two. Test on $one and $two. + $domain_user3 = $this->drupalCreateUser([ + 'access content', + 'update page content on assigned domains', + 'delete page content on assigned domains', + ]); + $this->addDomainsToEntity('user', $domain_user3->id(), $two, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $domain_user3 = $this->userStorage->load($domain_user3->id()); + $assigned = $this->manager->getAccessValues($domain_user3); + $this->assertCount(1, $assigned, 'User assigned to one domain.'); + $this->assertArrayHasKey($two, $assigned, 'User assigned to proper test domain.'); + + // Assign two different node types to our test domain. + $domain_node3 = $this->drupalCreateNode(['type' => 'article', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$two]]); + $domain_node4 = $this->drupalCreateNode(['type' => 'page', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$two]]); + $assigned = $this->manager->getAccessValues($domain_node3); + $this->assertArrayHasKey($two, $assigned, 'Node3 assigned to proper test domain.'); + $assigned = $this->manager->getAccessValues($domain_node4); + $this->assertArrayHasKey($two, $assigned, 'Node4 assigned to proper test domain.'); + + // Tests 'edit TYPE content on assigned domains'. + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => FALSE, + 'delete' => FALSE, + ], $domain_node3, $domain_user3); + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => TRUE, + 'delete' => TRUE, + ], $domain_node4, $domain_user3); + + // @TODO: Test edit and delete for user with 'all affiliates' permission. + // Tests 'edit domain TYPE content'. + // Assign our user to domain $two. Test on $one and $two. + $domain_user4 = $this->drupalCreateUser([ + 'access content', + 'update page content on assigned domains', + 'delete page content on assigned domains', + ]); + $this->addDomainsToEntity('user', $domain_user4->id(), $two, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $this->addDomainsToEntity('user', $domain_user4->id(), 1, DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD); + $domain_user4 = $this->userStorage->load($domain_user4->id()); + $assigned = $this->manager->getAccessValues($domain_user4); + $this->assertCount(1, $assigned, 'User assigned to one domain.'); + $this->assertArrayHasKey($two, $assigned, 'User assigned to proper test domain.'); + $this->assertNotEmpty($domain_user4->get(DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD)->value, 'User assign to all affiliates.'); + + // Assign two different node types to our test domain. + $domain_node5 = $this->drupalCreateNode(['type' => 'article', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$one]]); + $domain_node6 = $this->drupalCreateNode(['type' => 'page', DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$one]]); + $assigned = $this->manager->getAccessValues($domain_node5); + $this->assertArrayHasKey($one, $assigned, 'Node5 assigned to proper test domain.'); + $assigned = $this->manager->getAccessValues($domain_node6); + $this->assertArrayHasKey($one, $assigned, 'Node6 assigned to proper test domain.'); + + // Tests 'edit TYPE content on assigned domains'. + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => FALSE, + 'delete' => FALSE, + ], $domain_node5, $domain_user4); + $this->assertNodeAccess([ + 'view' => TRUE, + 'update' => TRUE, + 'delete' => TRUE, + ], $domain_node6, $domain_user4); + + } + + /** + * Tests domain access create permissions. + */ + public function testDomainAccessCreatePermissions() { + foreach ($this->domains as $domain) { + if (!isset($one)) { + $one = $domain->id(); + continue; + } + if (!isset($two)) { + $two = $domain->id(); + } + } + // Tests create permissions. Any content on assigned domains. + $domain_account5 = $this->drupalCreateUser(['access content', 'create domain content']); + $this->addDomainsToEntity('user', $domain_account5->id(), $two, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $domain_user5 = $this->userStorage->load($domain_account5->id()); + $assigned = $this->manager->getAccessValues($domain_user5); + $this->assertCount(1, $assigned, 'User assigned to one domain.'); + $this->assertArrayHasKey($two, $assigned, 'User assigned to proper test domain.'); + // This test is domain sensitive. + foreach ($this->domains as $domain) { + $this->domainLogin($domain, $domain_account5); + $url = $domain->getPath() . 'node/add/page'; + $this->drupalGet($url); + if ($domain->id() == $two) { + $this->assertResponse(200); + } + else { + $this->assertResponse(403); + } + // The user should be allowed to create articles. + $url = $domain->getPath() . 'node/add/article'; + $this->drupalGet($url); + if ($domain->id() == $two) { + $this->assertResponse(200); + } + else { + $this->assertResponse(403); + } + } + + } + + /** + * Tests domain access limited create permissions. + */ + public function testDomainAccessLimitedCreatePermissions() { + foreach ($this->domains as $domain) { + if (!isset($one)) { + $one = $domain->id(); + continue; + } + if (!isset($two)) { + $two = $domain->id(); + } + } + // Tests create permissions. Any content on assigned domains. + $domain_account6 = $this->drupalCreateUser(['access content', 'create page content on assigned domains']); + $this->addDomainsToEntity('user', $domain_account6->id(), $two, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + $domain_user6 = $this->userStorage->load($domain_account6->id()); + $assigned = $this->manager->getAccessValues($domain_user6); + $this->assertCount(1, $assigned, 'User assigned to one domain.'); + $this->assertArrayHasKey($two, $assigned, 'User assigned to proper test domain.'); + // This test is domain sensitive. + foreach ($this->domains as $domain) { + $this->domainLogin($domain, $domain_account6); + $url = $domain->getPath() . 'node/add/page'; + $this->drupalGet($url); + if ($domain->id() == $two) { + $this->assertResponse(200); + } + else { + $this->assertResponse(403); + } + // The user should not be allowed to create articles. + $url = $domain->getPath() . 'node/add/article'; + $this->drupalGet($url); + $this->assertResponse(403); + } + } + + /** + * Asserts that node access correctly grants or denies access. + * + * @param array $ops + * An associative array of the expected node access grants for the node + * and account, with each key as the name of an operation (e.g. 'view', + * 'delete') and each value a Boolean indicating whether access to that + * operation should be granted. + * @param \Drupal\node\NodeInterface $node + * The node object to check. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account for which to check access. + */ + public function assertNodeAccess(array $ops, NodeInterface $node, AccountInterface $account) { + foreach ($ops as $op => $result) { + $this->assertEqual($result, $this->accessHandler->access($node, $op, $account), 'Expected result returned.'); + } + } + +} diff --git a/domain_access/src/Tests/DomainAccessRecordsTest.php b/domain_access/tests/src/Functional/DomainAccessRecordsTest.php similarity index 69% rename from domain_access/src/Tests/DomainAccessRecordsTest.php rename to domain_access/tests/src/Functional/DomainAccessRecordsTest.php index 82437212..21a67687 100644 --- a/domain_access/src/Tests/DomainAccessRecordsTest.php +++ b/domain_access/tests/src/Functional/DomainAccessRecordsTest.php @@ -1,9 +1,10 @@ domainCreateTestDomains(5); // Assign a node to a random domain. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); $active_domain = array_rand($domains, 1); $domain = $domains[$active_domain]; $node_storage = \Drupal::entityTypeManager()->getStorage('node'); // Create an article node. - $node1 = $this->drupalCreateNode(array( + $node1 = $this->drupalCreateNode([ 'type' => 'article', - DOMAIN_ACCESS_FIELD => array($domain->id()), - DOMAIN_ACCESS_ALL_FIELD => 0, - )); - $this->assertTrue($node_storage->load($node1->id()), 'Article node created.'); + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$domain->id()], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 0, + ]); + $this->assertNotNull($node_storage->load($node1->id()), 'Article node created.'); // Check to see if grants added by domain_node_access_records made it in. $query = 'SELECT realm, gid, grant_view, grant_update, grant_delete FROM {node_access} WHERE nid = :nid'; $records = Database::getConnection() - ->query($query, array(':nid' => $node1->id())) + ->query($query, [':nid' => $node1->id()]) ->fetchAll(); - $this->assertEqual(count($records), 1, 'Returned the correct number of rows.'); + $this->assertCount(1, $records, 'Returned the correct number of rows.'); $this->assertEqual($records[0]->realm, 'domain_id', 'Grant with domain_id acquired for node.'); $this->assertEqual($records[0]->gid, $domain->getDomainId(), 'Grant with proper id acquired for node.'); $this->assertEqual($records[0]->grant_view, 1, 'Grant view stored.'); @@ -53,18 +54,18 @@ public function testDomainAccessRecords() { $this->assertEqual($records[0]->grant_delete, 1, 'Grant delete stored.'); // Create another article node. - $node2 = $this->drupalCreateNode(array( + $node2 = $this->drupalCreateNode([ 'type' => 'article', - DOMAIN_ACCESS_FIELD => array($domain->id()), - DOMAIN_ACCESS_ALL_FIELD => 1, - )); - $this->assertTrue($node_storage->load($node2->id()), 'Article node created.'); + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$domain->id()], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 1, + ]); + $this->assertNotNull($node_storage->load($node2->id()), 'Article node created.'); // Check to see if grants added by domain_node_access_records made it in. $query = 'SELECT realm, gid, grant_view, grant_update, grant_delete FROM {node_access} WHERE nid = :nid ORDER BY realm'; $records = Database::getConnection() - ->query($query, array(':nid' => $node2->id())) + ->query($query, [':nid' => $node2->id()]) ->fetchAll(); - $this->assertEqual(count($records), 2, 'Returned the correct number of rows.'); + $this->assertCount(2, $records, 'Returned the correct number of rows.'); $this->assertEqual($records[0]->realm, 'domain_id', 'Grant with domain_id acquired for node.'); $this->assertEqual($records[0]->gid, $domain->getDomainId(), 'Grant with proper id acquired for node.'); $this->assertEqual($records[0]->grant_view, 1, 'Grant view stored.'); diff --git a/domain_access/tests/src/Functional/DomainAccessSaveTest.php b/domain_access/tests/src/Functional/DomainAccessSaveTest.php new file mode 100644 index 00000000..d6eb217a --- /dev/null +++ b/domain_access/tests/src/Functional/DomainAccessSaveTest.php @@ -0,0 +1,77 @@ +domainCreateTestDomains(5); + } + + /** + * Basic test setup. + */ + public function testDomainAccessSave() { + $storage = \Drupal::entityTypeManager()->getStorage('node'); + // Save a node programatically. + $node = $storage->create([ + 'type' => 'article', + 'title' => 'Test node', + 'uid' => '1', + 'status' => 1, + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => ['example_com'], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 1, + ]); + $node->save(); + + // Load the node. + $node = $storage->load(1); + + // Check that two values are set properly. + $manager = \Drupal::service('domain_access.manager'); + $values = $manager->getAccessValues($node); + $this->assert(count($values) == 1, 'Node saved with one domain records.'); + $value = $manager->getAllValue($node); + $this->assert($value == 1, 'Node saved to all affiliates.'); + + // Save a node with different values. + $node = $storage->create([ + 'type' => 'article', + 'title' => 'Test node', + 'uid' => '1', + 'status' => 1, + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => ['example_com', 'one_example_com'], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 0, + ]); + $node->save(); + + // Load and check the node. + $node = $storage->load(2); + $values = $manager->getAccessValues($node); + $this->assert(count($values) == 2, 'Node saved with two domain records.'); + $value = $manager->getAllValue($node); + $this->assert($value == 0, 'Node not saved to all affiliates.'); + } + +} diff --git a/domain_access/tests/src/Kernel/DomainAccessEntityCrudTest.php b/domain_access/tests/src/Kernel/DomainAccessEntityCrudTest.php new file mode 100644 index 00000000..f0cee6f7 --- /dev/null +++ b/domain_access/tests/src/Kernel/DomainAccessEntityCrudTest.php @@ -0,0 +1,178 @@ +entityTypeManager = $this->container->get('entity_type.manager'); + + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('user'); + $this->installSchema('user', ['users_data']); + $this->installEntitySchema('node'); + $this->installEntitySchema('node_type'); + $this->installSchema('node', ['node_access']); + $this->installConfig($this::$modules); + + $type = $this->entityTypeManager->getStorage('node_type')->create(['type' => 'page', 'name' => 'page']); + $type->save(); + + module_load_install('domain_access'); + domain_access_install(); + } + + /** + * Delete domain access fields. + * + * @param string $entity_type + * Entity type. + * @param string $bundle + * Entity type bundle. + */ + protected function deleteDomainAccessFields($entity_type, $bundle) { + $fields = [DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD, DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD]; + foreach ($fields as $field_name) { + FieldConfig::loadByName($entity_type, $bundle, $field_name)->delete(); + } + } + + /** + * Tests node creation with installed domain access fields. + */ + public function testNodeCreateWithInstalledDomainAccessFields() { + $node = $this->drupalCreateNode(); + $node->save(); + self::assertNotEmpty($node->id()); + } + + /** + * Tests node creation with uninstalled domain access fields. + */ + public function testNodeCreateWithUninstalledDomainAccessFields() { + $this->deleteDomainAccessFields('node', 'page'); + + $node = $this->drupalCreateNode(); + $node->save(); + self::assertNotEmpty($node->id()); + } + + /** + * Tests node update with installed domain access fields. + */ + public function testNodeUpdateWithInstalledDomainAccessFields() { + $node = $this->drupalCreateNode(); + $node->save(); + self::assertNotEmpty($node->id()); + + $new_title = $this->randomMachineName(8); + $node->setTitle($new_title); + $node->save(); + self::assertSame($new_title, $node->getTitle()); + } + + /** + * Tests node update with uninstalled domain access fields. + */ + public function testNodeUpdateWithUninstalledDomainAccessFields() { + $node = $this->drupalCreateNode(); + $node->save(); + self::assertNotEmpty($node->id()); + + $this->deleteDomainAccessFields('node', 'page'); + $reloaded_node = $this->entityTypeManager->getStorage('node')->load($node->id()); + + $new_title = $this->randomMachineName(8); + $reloaded_node->setTitle($new_title); + $reloaded_node->save(); + self::assertSame($new_title, $reloaded_node->getTitle()); + } + + /** + * Tests user creation with installed domain access fields. + */ + public function testUserCreateWithInstalledDomainAccessFields() { + $user = $this->drupalCreateUser(); + $user->save(); + self::assertNotEmpty($user->id()); + } + + /** + * Tests user creation with uninstalled domain access fields. + */ + public function testUserCreateWithUninstalledDomainAccessFields() { + $this->deleteDomainAccessFields('user', 'user'); + + $user = $this->drupalCreateUser(); + $user->save(); + self::assertNotEmpty($user->id()); + } + + /** + * Tests user update with installed domain access fields. + */ + public function testUserUpdateWithInstalledDomainAccessFields() { + $node = $this->drupalCreateNode(); + $node->save(); + self::assertNotEmpty($node->id()); + + $new_title = $this->randomMachineName(8); + $node->setTitle($new_title); + $node->save(); + self::assertSame($new_title, $node->getTitle()); + } + + /** + * Tests user update with uninstalled domain access fields. + */ + public function testUserUpdateWithUninstalledDomainAccessFields() { + $user = $this->drupalCreateUser(); + $user->save(); + self::assertNotEmpty($user->id()); + + $this->deleteDomainAccessFields('user', 'user'); + $reloaded_user = $this->entityTypeManager->getStorage('user')->load($user->id()); + + $new_name = $this->randomMachineName(); + $reloaded_user->setUsername($new_name); + $reloaded_user->save(); + self::assertSame($new_name, $reloaded_user->getAccountName()); + } + +} diff --git a/domain_alias/README.md b/domain_alias/README.md index b4d844a6..eea1623b 100644 --- a/domain_alias/README.md +++ b/domain_alias/README.md @@ -58,7 +58,8 @@ Example request: `one.example.com` one.*.*, ``` Note that wildcard matching happens _in the listed order_. The number of -wildcards is equal to the number of hostname parts minus 1. +wildcards is equal to the number of hostname parts minus 1. That is, you cannot register +an alias that is all wildcards. Port Matching === @@ -89,7 +90,93 @@ specified. *.com:8080 *.com:* ``` + +Development Workflow +==== + +Aliases can be used to support development across different environments, with unique +URLs. To support this feature, there is now an `environment` field for each alias. The +default environment list is: + +* default +* local +* development +* staging +* testing + +This list may be overridden by setting the `domain_alias.environments` configuration in +settings.php. + +The operation of these environments is as follows: + +* If alias matching the environment is `default`, no changes occur. +* Else, matching aliases are loaded for all domains, so that links are rewritten to be +specific to the specified environment. (See `domain_alias_domain_load()` for the logic.) + +For instance, consider the following configuration. Your site's canonical domains are: + +* example.com +* foo.example.com +* bar.example.com + +When developing locally, developers use `.local` instead of `.com`. These should be +aliased to each domain as set as the `local` environment. + +* example.local > alias to example.com +* foo.example.local > alias to foo.example.com +* bar.example.local > alias to bar.example.com + +When pushing changes to the cloud, we use a development server. These are tied to a +specific cloud host (dev.mycloud.com). You can alias these to the `development` +environment. + +* example.dev.mycloud.com > alias to example.com +* foo.example.dev.mycloud.com > alias to foo.example.com +* bar.example.dev.mycloud.com > alias to bar.example.com + +The pattern can repeat for each of the environments listed above. The intended use of the +default set of environments is: + +* default -- indicates a canonical URL. No changes will be made. +* local -- for local development environments. +* development -- for a development integration server (such as those provided by Acquia, +Pantheon, and Platform.sh) +* staging -- for a pre-deployment server (such as those provided by Acquia, Panthon, and +Platform.sh) +* testing -- for continuous integration services (such as TravisCI or CircleCI). + +None of these environments are required. You may safely set all aliases to default if +your workflow does not span multiple server environments. + +How does it work? +---- + +This feature works by mapping each alias to an environment. If the active request matches +an alias that is set as an environment other than `default`, then matching environment +aliases are loaded for each domain. If a match is found, the `hostname` value for each +domain is overwritten. + +This overwrite affects the base path and request url that Domain module (and Domain Source) +use for writing links. + +Because the environments are specific to hostnames, this feature will only work if the +site's cache recognizes `url.site` as a required cache context. Without that, the render +system will cache the output of a request incorrectly. + +Configuration +---- + +To use this feature, the following steps must be followed: + +* `url.site` must be added as a required_cache_context to your `services.yml` file. +* Aliases must be mapped to a server environment. Default value is `default`. +* All aliases should be listed as part of `trusted_host_settings` in `settings.php`. + Technical Notes ==== -See DomainAliasSortTest for the logic. +The matching follows an explicit sort order shown in DomainAliasSortTest. + +The code will attempt to match domains of different lengths when doing wildcard +matching in an environment. That is, an alias to `example.*` assigned to `local` should +return `example.local` if the active domain is `one.example.local`, assigned to `local`. diff --git a/domain_alias/config/install/domain_alias.settings.yml b/domain_alias/config/install/domain_alias.settings.yml new file mode 100644 index 00000000..41b02b54 --- /dev/null +++ b/domain_alias/config/install/domain_alias.settings.yml @@ -0,0 +1,6 @@ +environments: + - default + - local + - development + - staging + - testing diff --git a/domain_alias/config/schema/domain_alias.schema.yml b/domain_alias/config/schema/domain_alias.schema.yml index 551ad6cb..8b032058 100644 --- a/domain_alias/config/schema/domain_alias.schema.yml +++ b/domain_alias/config/schema/domain_alias.schema.yml @@ -1,4 +1,14 @@ # Schema for the configuration files of the Domain Alias module. +domain_alias.settings: + type: config_object + label: 'Domain Alias settings' + mapping: + environments: + type: sequence + label: 'Development environments' + sequence: + type: string + label: 'Environment' domain_alias.alias.*: type: config_entity @@ -19,3 +29,6 @@ domain_alias.alias.*: redirect: type: integer label: 'Redirect' + environment: + type: string + label: 'Environment' diff --git a/domain_alias/domain_alias.info.yml b/domain_alias/domain_alias.info.yml index 2cd9123b..28e431a8 100644 --- a/domain_alias/domain_alias.info.yml +++ b/domain_alias/domain_alias.info.yml @@ -2,7 +2,12 @@ name: Domain Alias description: 'Maps multiple host requests to a single domain record.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - domain + - domain:domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_alias/domain_alias.install b/domain_alias/domain_alias.install new file mode 100644 index 00000000..6b2f06aa --- /dev/null +++ b/domain_alias/domain_alias.install @@ -0,0 +1,33 @@ +getEditable('domain_alias.settings'); + // Set and save new message value. + $environments = ['default', 'local', 'development', 'staging', 'testing']; + $config->set('environments', $environments)->save(); +} + +/** + * Updates domain_alias schema. + */ +function domain_alias_update_8001() { + $manager = \Drupal::entityDefinitionUpdateManager(); + // Regenerate entity type indexes. + $manager->updateEntityType($manager->getEntityType('domain_alias')); +} + +/** + * Adds domain_alias environment settings. + */ +function domain_alias_update_8002() { + // Set the default environments variable. + domain_alias_set_environments(); +} diff --git a/domain_alias/domain_alias.module b/domain_alias/domain_alias.module index fe7fb62a..e9f2583e 100644 --- a/domain_alias/domain_alias.module +++ b/domain_alias/domain_alias.module @@ -5,10 +5,11 @@ * Maps multiple host requests to a single domain record. */ -use Drupal\domain\DomainInterface; -use Drupal\domain\DomainNegotiator; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Url; use Drupal\Core\Session\AccountInterface; +use Drupal\domain\DomainInterface; +use Drupal\domain\DomainNegotiatorInterface; /** * Implements hook_domain_request_alter(). @@ -26,24 +27,37 @@ use Drupal\Core\Session\AccountInterface; * These patterns should be sufficient for most conditions. */ function domain_alias_domain_request_alter(DomainInterface &$domain) { + // During the installation the entity definition is not yet added when this + // hook is invoked, so skip if not present. + $has_definition = \Drupal::entityTypeManager()->hasDefinition('domain_alias'); + // If an exact match has loaded, do nothing. - if ($domain->getMatchType() == DomainNegotiator::DOMAIN_MATCH_EXACT) { + if ($domain->getMatchType() === DomainNegotiatorInterface::DOMAIN_MATCHED_EXACT || !$has_definition) { return; } // If no exact match, then run the alias load routine. $hostname = $domain->getHostname(); + $alias_storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); /** @var \Drupal\domain_alias\Entity\DomainAlias $alias */ - if ($alias = \Drupal::service('domain_alias.loader')->loadByHostname($hostname)) { + if ($alias = $alias_storage->loadByHostname($hostname)) { /** @var \Drupal\domain\Entity\Domain $domain */ - if ($domain = \Drupal::service('domain.loader')->load($alias->getDomainId())) { - $domain->addProperty('alias', $alias->getPattern()); - $domain->setMatchType(DomainNegotiator::DOMAIN_MATCH_ALIAS); + if ($domain = $domain_storage->load($alias->getDomainId())) { + $domain->addProperty('alias', $alias); + $domain->setMatchType(DomainNegotiatorInterface::DOMAIN_MATCHED_ALIAS); $redirect = $alias->getRedirect(); if (!empty($redirect)) { $domain->setRedirect($redirect); } } - // @TODO: error capture? + else { + // If the domain did not load, report an error. + \Drupal::logger('domain_alias')->error('Found matching alias %alias for host request %hostname, but failed to load matching domain with id %id.', [ + '%alias' => $alias->getPattern(), + '%hostname' => $hostname, + '%id' => $alias->getDomainId(), + ]); + } } } @@ -52,15 +66,108 @@ function domain_alias_domain_request_alter(DomainInterface &$domain) { */ function domain_alias_domain_operations(DomainInterface $domain, AccountInterface $account) { $operations = []; - // Check permissions. - if ($account->hasPermission('view domain aliases') || $account->hasPermission('administer domain aliases')) { - // Add aliases to the list. - $id = $domain->id(); - $operations['domain_alias'] = array( + // Check permissions. The user must be a super-admin or assigned to the + // domain. + $is_domain_admin = $domain->access('update', $account); + if ($account->hasPermission('administer domain aliases') || ($is_domain_admin && $account->hasPermission('view domain aliases'))) { + // Add aliases to the list of operations. + $operations['domain_alias'] = [ 'title' => t('Aliases'), - 'url' => Url::fromRoute('domain_alias.admin', array('domain' => $id)), + 'url' => Url::fromRoute('domain_alias.admin', ['domain' => $domain->id()]), 'weight' => 60, - ); + ]; } return $operations; } + +/** + * Implements hook_ENTITY_TYPE_load(). + */ +function domain_alias_domain_load($entities) { + static $enabled; + // We can only perform meaningful actions if url.site is a cache context. + // Otherwise, the render process ignores our changes. + if (!isset($enabled)) { + $required_cache_contexts = \Drupal::getContainer()->getParameter('renderer.config')['required_cache_contexts']; + if (!in_array('url.site', $required_cache_contexts) && !in_array('url', $required_cache_contexts)) { + $enabled = FALSE; + return; + } + $enabled = TRUE; + } + + // We cannot run before the negotiator service has fired. + $negotiator = \Drupal::service('domain.negotiator'); + $active = $negotiator->getActiveDomain(); + + // Do nothing if no domain is active. + if (empty($active)) { + return; + } + + // Load and rewrite environment-specific aliases. + $alias_storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); + if (isset($active->alias) && $active->alias->getEnvironment() != 'default') { + foreach ($entities as $id => $domain) { + if ($environment_aliases = $alias_storage->loadByEnvironmentMatch($domain, $active->alias->getEnvironment())) { + foreach ($environment_aliases as $environment_alias) { + $pattern = $environment_alias->getPattern(); + // Add a canonical property. + $domain->setCanonical(); + // Override the domain hostname and path. We always prefer a string + // match. + if (substr_count($pattern, '*') < 1) { + $domain->setHostname($pattern); + $domain->setPath(); + $domain->setUrl(); + break; + } + else { + // Do a wildcard replacement based on the current request. + $request = $negotiator->negotiateActiveHostname(); + // First, check for a wildcard port. + if (substr_count($pattern, ':*') > 0) { + // Do not replace ports unless they are nonstandard. See + // \Symfony\Component\HttpFoundation\Request\getHttpHost(). + if (substr_count($request, ':') > 0) { + $search = explode(':', $pattern); + $replace = explode(':', $request); + if (!empty($search[1]) && !empty($replace[1])) { + $pattern = str_replace(':' . $search[1], ':' . $replace[1], $pattern); + } + } + // If no port wildcard, then remove the port entirely. + else { + $pattern = str_replace(':*', '', $pattern); + } + } + + $replacements = ['.' => '\.', '*' => '(.+?)']; + $regex = '/^' . strtr($active->alias->getPattern(), $replacements) . '$/'; + if (preg_match($regex, $request, $matches) && isset($matches[1])) { + $pattern = str_replace('*', $matches[1], $pattern); + } + + // Do not let the domain loop back on itself. + if ($pattern != $domain->getCanonical()) { + $domain->setHostname($pattern); + $domain->setPath(); + $domain->setUrl(); + } + } + } + } + } + } +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function domain_alias_domain_delete(EntityInterface $entity) { + $alias_storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $properties = ['domain_id' => $entity->id()]; + foreach ($alias_storage->loadByProperties($properties) as $alias) { + $alias->delete(); + } +} diff --git a/domain_alias/domain_alias.permissions.yml b/domain_alias/domain_alias.permissions.yml index 7f153ecd..88fd5765 100644 --- a/domain_alias/domain_alias.permissions.yml +++ b/domain_alias/domain_alias.permissions.yml @@ -1,10 +1,14 @@ administer domain aliases: title: 'Administer all domain aliases' + restrict access: true create domain aliases: - title: 'Create domain aliases' + title: 'Create domain aliases for assigned domains' + restrict access: true edit domain aliases: - title: 'Edit domain aliases' + title: 'Edit domain aliases for assigned domains' + restrict access: true delete domain aliases: - title: 'Delete domain aliases' + title: 'Delete domain aliases for assigned domains' + restrict access: true view domain aliases: title: 'View aliases for assigned domains' diff --git a/domain_alias/domain_alias.services.yml b/domain_alias/domain_alias.services.yml index ab75e38b..efd4aa2e 100644 --- a/domain_alias/domain_alias.services.yml +++ b/domain_alias/domain_alias.services.yml @@ -1,11 +1,4 @@ services: - domain_alias.loader: - class: Drupal\domain_alias\DomainAliasLoader - tags: - - { name: persist } - arguments: ['@config.typed'] domain_alias.validator: class: Drupal\domain_alias\DomainAliasValidator - tags: - - { name: persist } - arguments: ['@config.factory'] + arguments: ['@config.factory', '@entity_type.manager'] diff --git a/domain_alias/src/Controller/DomainAliasController.php b/domain_alias/src/Controller/DomainAliasController.php index 647de518..93d1693a 100644 --- a/domain_alias/src/Controller/DomainAliasController.php +++ b/domain_alias/src/Controller/DomainAliasController.php @@ -2,13 +2,13 @@ namespace Drupal\domain_alias\Controller; +use Drupal\Core\Controller\ControllerBase; use Drupal\domain\DomainInterface; -use Drupal\domain\Controller\DomainControllerBase; /** * Returns responses for Domain Alias module routes. */ -class DomainAliasController extends DomainControllerBase { +class DomainAliasController extends ControllerBase { /** * Provides the domain alias submission form. @@ -23,8 +23,9 @@ public function addAlias(DomainInterface $domain) { // The entire purpose of this controller is to add the values from // the parent domain entity. $values['domain_id'] = $domain->id(); - // @TODO: ensure that this value is present in all cases. - $alias = \Drupal::entityTypeManager()->getStorage('domain_alias')->create($values); + + // Create the stub alias with reference to the parent domain. + $alias = $this->entityTypeManager()->getStorage('domain_alias')->create($values); return $this->entityFormBuilder()->getForm($alias); } @@ -39,7 +40,7 @@ public function addAlias(DomainInterface $domain) { * A render array as expected by drupal_render(). */ public function listing(DomainInterface $domain) { - $list = \Drupal::entityTypeManager()->getListBuilder('domain_alias'); + $list = $this->entityTypeManager()->getListBuilder('domain_alias'); $list->setDomain($domain); return $list->render(); } diff --git a/domain_alias/src/DomainAliasAccessControlHandler.php b/domain_alias/src/DomainAliasAccessControlHandler.php index 40e5228f..9e1d5c20 100644 --- a/domain_alias/src/DomainAliasAccessControlHandler.php +++ b/domain_alias/src/DomainAliasAccessControlHandler.php @@ -3,16 +3,14 @@ namespace Drupal\domain_alias; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\domain\DomainAccessControlHandler; /** * Defines the access controller for the domain alias entity type. - * - * Note that this is not a node access check. */ -class DomainAliasAccessControlHandler extends EntityAccessControlHandler { +class DomainAliasAccessControlHandler extends DomainAccessControlHandler { /** * {@inheritdoc} @@ -23,14 +21,24 @@ public function checkAccess(EntityInterface $entity, $operation, AccountInterfac if ($account->hasPermission('administer domain aliases')) { return AccessResult::allowed(); } - if ($operation == 'create' && $account->hasPermission('create domain aliases')) { - return AccessResult::allowed(); - } - if ($operation == 'update' && $account->hasPermission('edit domain aliases')) { - return AccessResult::allowed(); - } - if ($operation == 'delete' && $account->hasPermission('delete domain aliases')) { - return AccessResult::allowed(); + // For other actions we allow admin if they can administer the parent + // domains. + $domain = $entity->getDomain(); + // If this account can administer the domain, allow access to actions based + // on permission. + if (!empty($domain) && $this->isDomainAdmin($domain, $account)) { + if ($operation == 'view' && $account->hasPermission('view domain aliases')) { + return AccessResult::allowed(); + } + if ($operation == 'create' && $account->hasPermission('create domain aliases')) { + return AccessResult::allowed(); + } + if ($operation == 'update' && $account->hasPermission('edit domain aliases')) { + return AccessResult::allowed(); + } + if ($operation == 'delete' && $account->hasPermission('delete domain aliases')) { + return AccessResult::allowed(); + } } return AccessResult::forbidden(); } diff --git a/domain_alias/src/DomainAliasForm.php b/domain_alias/src/DomainAliasForm.php index 0c136794..27928b0f 100644 --- a/domain_alias/src/DomainAliasForm.php +++ b/domain_alias/src/DomainAliasForm.php @@ -2,8 +2,12 @@ namespace Drupal\domain_alias; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\domain\DomainStorageInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base form controller for domain alias edit forms. @@ -11,17 +15,90 @@ class DomainAliasForm extends EntityForm { /** - * Overrides Drupal\Core\Entity\EntityForm::form(). + * The domain alias validator. + * + * @var \Drupal\domain_alias\DomainAliasValidatorInterface + */ + protected $validator; + + /** + * The configuration factory service. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $config; + + /** + * The domain entity access control handler. + * + * @var \Drupal\domain\DomainAccessControlHandler + */ + protected $accessHandler; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The domain storage manager. + * + * @var \Drupal\domain\DomainStorageInterface + */ + protected $domainStorage; + + /** + * The domain alias storage manager. + * + * @var \Drupal\domain_alias\DomainAliasStorageInterface + */ + protected $aliasStorage; + + /** + * Constructs a DomainAliasForm object. + * + * @param \Drupal\domain_alias\DomainAliasValidatorInterface $validator + * The domain alias validator. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config + * The configuration factory service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(DomainAliasValidatorInterface $validator, ConfigFactoryInterface $config, EntityTypeManagerInterface $entity_type_manager) { + $this->validator = $validator; + $this->config = $config; + $this->entityTypeManager = $entity_type_manager; + $this->aliasStorage = $entity_type_manager->getStorage('domain_alias'); + $this->domainStorage = $entity_type_manager->getStorage('domain'); + // Not loaded directly since it is not an interface. + $this->accessHandler = $this->entityTypeManager->getAccessControlHandler('domain'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('domain_alias.validator'), + $container->get('config.factory'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { /** @var \Drupal\domain_alias\DomainAliasInterface $alias */ $alias = $this->entity; - $form['domain_id'] = array( + $form['domain_id'] = [ '#type' => 'value', '#value' => $alias->getDomainId(), - ); - $form['pattern'] = array( + ]; + $form['pattern'] = [ '#type' => 'textfield', '#title' => $this->t('Pattern'), '#size' => 40, @@ -29,21 +106,69 @@ public function form(array $form, FormStateInterface $form_state) { '#default_value' => $alias->getPattern(), '#description' => $this->t('The matching pattern for this alias.'), '#required' => TRUE, - ); - $form['id'] = array( + ]; + $form['id'] = [ '#type' => 'machine_name', '#default_value' => $alias->id(), - '#machine_name' => array( - 'source' => array('pattern'), + '#machine_name' => [ + 'source' => ['pattern'], 'exists' => '\Drupal\domain_alias\Entity\DomainAlias::load', - ), - ); - $form['redirect'] = array( + ], + ]; + $form['redirect'] = [ '#type' => 'select', '#options' => $this->redirectOptions(), '#default_value' => $alias->getRedirect(), - '#description' => $this->t('Redirect status'), - ); + '#description' => $this->t('Set an optional redirect directive when this alias is invoked.'), + ]; + $environments = $this->environmentOptions(); + $form['environment'] = [ + '#type' => 'select', + '#options' => $environments, + '#default_value' => $alias->getEnvironment(), + '#description' => $this->t('Map the alias to a development environment.'), + ]; + $form['environment_help'] = [ + '#type' => 'details', + '#open' => FALSE, + '#collapsed' => TRUE, + '#title' => $this->t('Environment list'), + '#description' => $this->t('The table below shows the registered aliases for each environment.'), + ]; + + $domains = $this->domainStorage->loadMultipleSorted(); + $rows = []; + foreach ($domains as $domain) { + // If the user cannot edit the domain, then don't show in the list. + $access = $this->accessHandler->checkAccess($domain, 'update'); + if ($access->isForbidden()) { + continue; + } + $row = []; + $row[] = $domain->label(); + foreach ($environments as $environment) { + $match_output = []; + if ($environment == 'default') { + $match_output[] = $domain->getCanonical(); + } + $matches = $this->aliasStorage->loadByEnvironmentMatch($domain, $environment); + foreach ($matches as $match) { + $match_output[] = $match->getPattern(); + } + $output = [ + '#items' => $match_output, + '#theme' => 'item_list', + ]; + $row[] = \Drupal::service('renderer')->render($output); + } + $rows[] = $row; + } + + $form['environment_help']['table'] = [ + '#type' => 'table', + '#header' => array_merge([$this->t('Domain')], $environments), + '#rows' => $rows, + ]; return parent::form($form, $form_state); } @@ -55,51 +180,54 @@ public function form(array $form, FormStateInterface $form_state) { * A list of valid redirect options. */ public function redirectOptions() { - return array( + return [ 0 => $this->t('Do not redirect'), 301 => $this->t('301 redirect: Moved Permanently'), 302 => $this->t('302 redirect: Found'), - ); + ]; } /** - * Overrides \Drupal\Core\Entity\EntityForm::validate(). + * Returns a list of valid environement options for the form. + * + * @return array + * A list of valid environment options. + */ + public function environmentOptions() { + $list = $this->config->get('domain_alias.settings')->get('environments'); + $environments = []; + foreach ($list as $item) { + $environments[$item] = $item; + } + return $environments; + } + + /** + * {@inheritdoc} */ - public function validate(array $form, FormStateInterface $form_state) { - $entity = $this->buildEntity($form, $form_state); - $validator = \Drupal::service('domain_alias.validator'); - $errors = $validator->validate($entity); + public function validateForm(array &$form, FormStateInterface $form_state) { + $errors = $this->validator->validate($this->entity); if (!empty($errors)) { $form_state->setErrorByName('pattern', $errors); } } /** - * Overrides Drupal\Core\Entity\EntityForm::save(). + * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { /** @var \Drupal\domain_alias\DomainAliasInterface $alias */ $alias = $this->entity; - if ($alias->isNew()) { - drupal_set_message($this->t('Domain alias created.')); + $edit_link = $alias->toLink($this->t('Edit'), 'edit-form')->toString(); + if ($alias->save() == SAVED_NEW) { + \Drupal::messenger()->addMessage($this->t('Created new domain alias.')); + $this->logger('domain_alias')->notice('Created new domain alias %name.', ['%name' => $alias->label(), 'link' => $edit_link]); } else { - drupal_set_message($this->t('Domain alias updated.')); + \Drupal::messenger()->addMessage($this->t('Updated domain alias.')); + $this->logger('domain_alias')->notice('Updated domain alias %name.', ['%name' => $alias->label(), 'link' => $edit_link]); } - $alias->save(); - $form_state->setRedirect('domain_alias.admin', array('domain' => $alias->getDomainId())); - } - - /** - * Overrides Drupal\Core\Entity\EntityForm::delete(). - */ - public function delete(array $form, FormStateInterface $form_state) { - /** @var \Drupal\domain_alias\DomainAliasInterface $alias */ - $alias = $this->entity; - // @TODO: error handling? - $alias->delete(); - - $form_state->setRedirect('domain_alias.admin', array('domain' => $alias->getDomainId())); + $form_state->setRedirect('domain_alias.admin', ['domain' => $alias->getDomainId()]); } } diff --git a/domain_alias/src/DomainAliasInterface.php b/domain_alias/src/DomainAliasInterface.php index d9ba0a90..b4f341ab 100644 --- a/domain_alias/src/DomainAliasInterface.php +++ b/domain_alias/src/DomainAliasInterface.php @@ -25,6 +25,14 @@ public function getPattern(); */ public function getDomainId(); + /** + * Get the parent domain entity for an alias record. + * + * @return \Drupal\domain\Entity\Domain + * The parent domain for the alias record or NULL if not set. + */ + public function getDomain(); + /** * Get the redirect value (301|302|NULL) for an alias record. * diff --git a/domain_alias/src/DomainAliasListBuilder.php b/domain_alias/src/DomainAliasListBuilder.php index b54f504c..ff9837d1 100644 --- a/domain_alias/src/DomainAliasListBuilder.php +++ b/domain_alias/src/DomainAliasListBuilder.php @@ -14,7 +14,7 @@ class DomainAliasListBuilder extends ConfigEntityListBuilder { /** * A domain object loaded from the controller. * - * @var DomainInterface $domain + * @var \Drupal\domain\DomainInterface */ protected $domain; @@ -24,6 +24,7 @@ class DomainAliasListBuilder extends ConfigEntityListBuilder { public function buildHeader() { $header['label'] = $this->t('Pattern'); $header['redirect'] = $this->t('Redirect'); + $header['environment'] = $this->t('Environment'); return $header + parent::buildHeader(); } @@ -31,11 +32,12 @@ public function buildHeader() { * {@inheritdoc} */ public function buildRow(EntityInterface $entity) { - $row = array(); + $row = []; $row['label'] = $entity->label(); $redirect = $entity->getRedirect(); $row['redirect'] = empty($redirect) ? $this->t('None') : $redirect; + $row['environment'] = $entity->getEnvironment(); $row += parent::buildRow($entity); return $row; @@ -45,12 +47,12 @@ public function buildRow(EntityInterface $entity) { * {@inheritdoc} */ public function render() { - $build = array( + $build = [ '#theme' => 'table', '#header' => $this->buildHeader(), - '#rows' => array(), + '#rows' => [], '#empty' => $this->t('No aliases have been created for this domain.'), - ); + ]; foreach ($this->load() as $entity) { if ($row = $this->buildRow($entity)) { $build['#rows'][$entity->id()] = $row; @@ -90,7 +92,7 @@ public function setDomain(DomainInterface $domain) { /** * Gets the domain context for this list. * - * @return \Drupal\domain\DomainInterface $domain + * @return \Drupal\domain\DomainInterface * The domain that is context for this list. */ public function getDomainId() { diff --git a/domain_alias/src/DomainAliasLoader.php b/domain_alias/src/DomainAliasStorage.php similarity index 56% rename from domain_alias/src/DomainAliasLoader.php rename to domain_alias/src/DomainAliasStorage.php index 7dab45e9..e39d3d4d 100644 --- a/domain_alias/src/DomainAliasLoader.php +++ b/domain_alias/src/DomainAliasStorage.php @@ -2,12 +2,17 @@ namespace Drupal\domain_alias; +use Drupal\Core\Config\Entity\ConfigEntityStorage; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\domain\DomainInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Alias loader utility class. */ -class DomainAliasLoader implements DomainAliasLoaderInterface { +class DomainAliasStorage extends ConfigEntityStorage implements DomainAliasStorageInterface { /** * The typed config handler. @@ -17,47 +22,50 @@ class DomainAliasLoader implements DomainAliasLoaderInterface { protected $typedConfig; /** - * Constructs a DomainAliasLoader object. + * The request stack object. * - * Trying to inject the storage manager throws an exception. + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + + /** + * Sets the TypedConfigManager dependency. * * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config * The typed config handler. - * - * @see getStorage() */ - public function __construct(TypedConfigManagerInterface $typed_config) { + protected function setTypedConfigManager(TypedConfigManagerInterface $typed_config) { $this->typedConfig = $typed_config; } /** - * {@inheritdoc} + * Sets the request stack object dependency. + * + * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack + * The request stack object. */ - public function loadSchema() { - $fields = $this->typedConfig->getDefinition('domain_alias.alias.*'); - return isset($fields['mapping']) ? $fields['mapping'] : array(); + protected function setRequestStack(RequestStack $request_stack) { + $this->requestStack = $request_stack; } /** * {@inheritdoc} */ - public function load($id, $reset = FALSE) { - $controller = $this->getStorage(); - if ($reset) { - $controller->resetCache(array($id)); - } - return $controller->load($id); + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + $instance = parent::createInstance($container, $entity_type); + $instance->setTypedConfigManager($container->get('config.typed')); + $instance->setRequestStack($container->get('request_stack')); + + return $instance; } /** * {@inheritdoc} */ - public function loadMultiple($ids = NULL, $reset = FALSE) { - $controller = $this->getStorage(); - if ($reset) { - $controller->resetCache($ids); - } - return $controller->loadMultiple($ids); + public function loadSchema() { + $fields = $this->typedConfig->getDefinition('domain_alias.alias.*'); + return isset($fields['mapping']) ? $fields['mapping'] : []; } /** @@ -77,13 +85,35 @@ public function loadByHostname($hostname) { * {@inheritdoc} */ public function loadByPattern($pattern) { - $result = $this->getStorage()->loadByProperties(array('pattern' => $pattern)); + $result = $this->loadByProperties(['pattern' => $pattern]); if (empty($result)) { return NULL; } return current($result); } + /** + * {@inheritdoc} + */ + public function loadByEnvironment($environment) { + $result = $this->loadByProperties(['environment' => $environment]); + if (empty($result)) { + return NULL; + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadByEnvironmentMatch(DomainInterface $domain, $environment) { + $result = $this->loadByProperties(['domain_id' => $domain->id(), 'environment' => $environment]); + if (empty($result)) { + return []; + } + return $result; + } + /** * {@inheritdoc} */ @@ -103,27 +133,31 @@ public function sort($a, $b) { * A hostname string, in the format example.com. * * @return array + * An array of eligible matching patterns. */ public function getPatterns($hostname) { $parts = explode('.', $hostname); $count = count($parts); + // Account for ports. + $port = NULL; if (substr_count($hostname, ':') > 0) { + // Extract port and save for later. $ports = explode(':', $parts[$count - 1]); $parts[$count - 1] = preg_replace('/:(\d+)/', '', $parts[$count - 1]); - $parts[] = $ports[1]; + $port = $ports[1]; } + // Build the list of possible matching patterns. $patterns = $this->buildPatterns($parts); // Pattern lists are sorted based on the fewest wildcards. That gives us // more precise matches first. - uasort($patterns, array($this, 'sort')); - array_unshift($patterns, $hostname); + uasort($patterns, [$this, 'sort']); + // Re-assemble parts without port + array_unshift($patterns, implode('.', $parts)); // Account for ports. - if (isset($ports)) { - $patterns = $this->buildPortPatterns($patterns, $hostname); - } + $patterns = $this->buildPortPatterns($patterns, $hostname, $port); // Return unique patters. return array_unique($patterns); @@ -135,7 +169,7 @@ public function getPatterns($hostname) { * @param array $parts * The hostname of the request, as an array split by dots. * - * @return array $patterns + * @return array * An array of eligible matching patterns. */ private function buildPatterns(array $parts) { @@ -146,12 +180,12 @@ private function buildPatterns(array $parts) { $temp[$i] = '*'; $patterns[] = implode('.', $temp); // Advanced multi-value wildcards. - // Pattern *.* + // Pattern *.*. if (count($temp) > 2 && $i < ($count - 1)) { $temp[$i + 1] = '*'; $patterns[] = implode('.', $temp); } - // Pattern foo.bar.* + // Pattern foo.bar.*. if ($count > 3 && $i < ($count - 2)) { $temp[$i + 2] = '*'; $patterns[] = implode('.', $temp); @@ -163,7 +197,7 @@ private function buildPatterns(array $parts) { $temp[$i + 2] = '*'; $patterns[] = implode('.', $temp); } - // Pattern *.foo.*.* + // Pattern *.foo.*.*. if ($count > 2) { $temp = array_fill(0, $count, '*'); $temp[$i] = $parts[$i]; @@ -180,54 +214,31 @@ private function buildPatterns(array $parts) { * An array of eligible matching patterns. * @param string $hostname * A hostname string, in the format example.com. + * @param integer $port + * The port of the request. * - * @return array $patterns + * @return array * An array of eligible matching patterns, modified by port. */ - private function buildPortPatterns(array $patterns, $hostname) { + private function buildPortPatterns(array $patterns, $hostname, $port = NULL) { + // Fetch the port if empty. + if (empty($port) && !empty($this->requestStack->getCurrentRequest())) { + $port = $this->requestStack->getCurrentRequest()->getPort(); + } + + $new_patterns = []; foreach ($patterns as $index => $pattern) { - // Make a pattern for port wildcards. - if (substr_count($pattern, ':') < 1) { - $new = explode('.', $pattern); - $port = (int) array_pop($new); - $allow = FALSE; - // Do not allow *.* or *:*. - foreach ($new as $item) { - if ($item != '*') { - $allow = TRUE; - } - } - if ($allow) { - // For port 80, allow bare hostname matches. - if ($port == 80) { - // Base hostname with port. - $patterns[] = str_replace(':' . $port, '', $hostname); - // Base hostname is allowed. - $patterns[] = implode('.', $new); - } - // Base hostname with wildcard port. - $patterns[] = str_replace(':' . $port, ':*', $hostname); - // Pattern with exact port. - $patterns[] = implode('.', $new) . ':' . $port; - // Pattern with wildcard port. - $patterns[] = implode('.', $new) . ':*'; - } - unset($patterns[$index]); + // If default ports, allow exact no-port alias + $new_patterns[] = $pattern . ':*'; + if (empty($port) || $port == 80 || $port == 443) { + $new_patterns[] = $pattern; + } + if (!empty($port)) { + $new_patterns[] = $pattern . ':' . $port; } } - return $patterns; - } - /** - * Loads the storage controller. - * - * We use the loader very early in the request cycle. As a result, if we try - * to inject the storage container, we hit a circular dependency. Using this - * method at least keeps our code easier to update. - */ - protected function getStorage() { - $storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); - return $storage; + return $new_patterns; } } diff --git a/domain_alias/src/DomainAliasLoaderInterface.php b/domain_alias/src/DomainAliasStorageInterface.php similarity index 50% rename from domain_alias/src/DomainAliasLoaderInterface.php rename to domain_alias/src/DomainAliasStorageInterface.php index ebd2ac08..8bf6d193 100644 --- a/domain_alias/src/DomainAliasLoaderInterface.php +++ b/domain_alias/src/DomainAliasStorageInterface.php @@ -2,36 +2,13 @@ namespace Drupal\domain_alias; +use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; +use Drupal\domain\DomainInterface; + /** - * Supplies loader methods for common domain_alias requests. + * Supplies storage methods for common domain_alias requests. */ -interface DomainAliasLoaderInterface { - - /** - * Loads a single alias. - * - * @param string $id - * A domain_alias id to load. - * @param bool $reset - * Indicates that the entity cache should be reset. - * - * @return DomainAliasInterface - * A Drupal\domain_alias\DomainAliasInterface object | NULL. - */ - public function load($id, $reset = FALSE); - - /** - * Loads multiple aliases. - * - * @param array $ids - * An optional array of specific ids to load. - * @param bool $reset - * Indicates that the entity cache should be reset. - * - * @return array - * An array of Drupal\domain_alias\DomainAliasInterface objects. - */ - public function loadMultiple($ids = NULL, $reset = FALSE); +interface DomainAliasStorageInterface extends ConfigEntityStorageInterface { /** * Loads a domain alias record by hostname lookup. @@ -41,7 +18,7 @@ public function loadMultiple($ids = NULL, $reset = FALSE); * @param string $hostname * A hostname string, in the format example.com. * - * @return \Drupal\domain_alias\DomainAliasInterface | NULL + * @return \Drupal\domain_alias\DomainAliasInterface|null * The best match alias record for the provided hostname. */ public function loadByHostname($hostname); @@ -52,11 +29,35 @@ public function loadByHostname($hostname); * @param string $pattern * A pattern string, in the format *.example.com. * - * @return \Drupal\domain_alias\DomainAliasInterface | NULL + * @return \Drupal\domain_alias\DomainAliasInterface|null * The domain alias record given a pattern string. */ public function loadByPattern($pattern); + /** + * Loads an array of domain alias record by environment lookup. + * + * @param string $environment + * An environment string, e.g. 'default' or 'local'. + * + * @return array + * An array of \Drupal\domain_alias\DomainAliasInterface objects. + */ + public function loadByEnvironment($environment); + + /** + * Loads a domain alias record by pattern lookup. + * + * @param \Drupal\domain\DomainInterface $domain + * A domain entity. + * @param string $environment + * An environment string, e.g. 'default' or 'local'. + * + * @return array + * An array of \Drupal\domain_alias\DomainAliasInterface objects. + */ + public function loadByEnvironmentMatch(DomainInterface $domain, $environment); + /** * Sorts aliases by wildcard to float exact matches to the top. * diff --git a/domain_alias/src/DomainAliasValidator.php b/domain_alias/src/DomainAliasValidator.php index 78d3c568..1f42182e 100644 --- a/domain_alias/src/DomainAliasValidator.php +++ b/domain_alias/src/DomainAliasValidator.php @@ -3,6 +3,7 @@ namespace Drupal\domain_alias; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** @@ -20,15 +21,39 @@ class DomainAliasValidator implements DomainAliasValidatorInterface { protected $configFactory; /** - * Constructs a DomainLoader object. + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The domain alias storage. + * + * @var \Drupal\domain_alias\DomainAliasStorageInterface + */ + protected $aliasStorage; + + /** + * The domain storage. + * + * @var \Drupal\domain\DomainStorageInterface + */ + protected $domainStorage; + + /** + * Constructs a domainStorage object. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The config factory. - * - * @see getStorage() + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(ConfigFactoryInterface $config_factory) { + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager) { $this->configFactory = $config_factory; + $this->entityTypeManager = $entity_type_manager; + $this->aliasStorage = $this->entityTypeManager->getStorage('domain_alias'); + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); } /** @@ -37,29 +62,36 @@ public function __construct(ConfigFactoryInterface $config_factory) { * @param \Drupal\domain_alias\DomainAliasInterface $alias * The Domain Alias to validate. * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup | NULL + * @return \Drupal\Core\StringTranslation\TranslatableMarkup * A validation error message, if any. */ public function validate(DomainAliasInterface $alias) { $pattern = $alias->getPattern(); - // 1) Check that the alias only has one wildcard. + // 1) Check for at least one dot or the use of 'localhost'. + // Note that localhost can specify a port. + $localhost_check = explode(':', $pattern); + if (substr_count($pattern, '.') == 0 && $localhost_check[0] != 'localhost') { + return $this->t('At least one dot (.) is required, except when using localhost.'); + } + + // 2) Check that the alias only has one wildcard. $count = substr_count($pattern, '*') + substr_count($pattern, '?'); if ($count > 1) { return $this->t('You may only have one wildcard character in each alias.'); } - // 2) Only one colon allowed, and it must be followed by an integer. + // 3) Only one colon allowed, and it must be followed by an integer. $count = substr_count($pattern, ':'); if ($count > 1) { return $this->t('You may only have one colon ":" character in each alias.'); } elseif ($count == 1) { $int = substr($pattern, strpos($pattern, ':') + 1); - if (!is_numeric($int)) { - return $this->t('A colon may only be followed by an integer indicating the proper port.'); + if (!is_numeric($int) && $int !== '*') { + return $this->t('A colon may only be followed by an integer indicating the proper port or the wildcard character (*).'); } } - // 3) Check that the alias doesn't contain any invalid characters. + // 4) Check that the alias doesn't contain any invalid characters. // Check for valid characters, unless using non-ASCII domains. $non_ascii = $this->configFactory->get('domain.settings')->get('allow_non_ascii'); if (!$non_ascii) { @@ -68,7 +100,7 @@ public function validate(DomainAliasInterface $alias) { return $this->t('The pattern contains invalid characters.'); } } - // 4) The alias cannot begin or end with a period. + // 5) The alias cannot begin or end with a period. if (substr($pattern, 0, 1) == '.') { return $this->t('The pattern cannot begin with a dot.'); } @@ -76,13 +108,13 @@ public function validate(DomainAliasInterface $alias) { return $this->t('The pattern cannot end with a dot.'); } - // 5) Check that the alias is not a direct match for a registered domain. + // 6) Check that the alias is not a direct match for a registered domain. $check = preg_match('/[a-z0-9\.\+\-:]*$/', $pattern); - if ($check == 1 && \Drupal::service('domain.loader')->loadByHostname($pattern)) { + if ($check == 1 && $this->domainStorage->loadByHostname($pattern)) { return $this->t('The pattern matches an existing domain record.'); } - // 6) Check that the alias is unique across all records. - if ($alias_check = \Drupal::service('domain_alias.loader')->loadByPattern($pattern)) { + // 7) Check that the alias is unique across all records. + if ($alias_check = $this->aliasStorage->loadByPattern($pattern)) { /** @var \Drupal\domain_alias\DomainAliasInterface $alias_check */ if ($alias_check->id() != $alias->id()) { return $this->t('The pattern already exists.'); diff --git a/domain_alias/src/DomainAliasValidatorInterface.php b/domain_alias/src/DomainAliasValidatorInterface.php index 2fbfcd1f..99c75637 100644 --- a/domain_alias/src/DomainAliasValidatorInterface.php +++ b/domain_alias/src/DomainAliasValidatorInterface.php @@ -13,7 +13,7 @@ interface DomainAliasValidatorInterface { * @param \Drupal\domain_alias\DomainAliasInterface $alias * The domain alias to validate. * - * @return \Drupal\Core\StringTranslation\TranslatableMarkup | NULL + * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null * The validation error message, if any. */ public function validate(DomainAliasInterface $alias); diff --git a/domain_alias/src/Entity/DomainAlias.php b/domain_alias/src/Entity/DomainAlias.php index 9864dff4..334d8046 100644 --- a/domain_alias/src/Entity/DomainAlias.php +++ b/domain_alias/src/Entity/DomainAlias.php @@ -14,7 +14,7 @@ * label = @Translation("Domain alias"), * module = "domain_alias", * handlers = { - * "storage" = "Drupal\Core\Config\Entity\ConfigEntityStorage", + * "storage" = "Drupal\domain_alias\DomainAliasStorage", * "access" = "Drupal\domain_alias\DomainAliasAccessControlHandler", * "list_builder" = "Drupal\domain_alias\DomainAliasListBuilder", * "form" = { @@ -30,6 +30,7 @@ * "domain_id" = "domain_id", * "label" = "pattern", * "uuid" = "uuid", + * "environment" = "environment", * }, * links = { * "delete-form" = "/admin/config/domain/alias/delete/{domain_alias}", @@ -40,6 +41,7 @@ * "domain_id", * "pattern", * "redirect", + * "environment", * } * ) */ @@ -76,10 +78,17 @@ class DomainAlias extends ConfigEntityBase implements DomainAliasInterface { /** * The domain alias record redirect value. * - * @var integer + * @var int */ protected $redirect; + /** + * The domain alias record environment value. + * + * @var string + */ + protected $environment; + /** * {@inheritdoc} */ @@ -87,6 +96,13 @@ public function getPattern() { return $this->pattern; } + /** + * {@inheritdoc} + */ + public function getEnvironment() { + return !empty($this->environment) ? $this->environment : 'default'; + } + /** * {@inheritdoc} */ @@ -94,6 +110,15 @@ public function getDomainId() { return $this->domain_id; } + /** + * {@inheritdoc} + */ + public function getDomain() { + $storage = \Drupal::entityTypeManager()->getStorage('domain'); + $domains = $storage->loadByProperties(['domain_id' => $this->domain_id]); + return $domains ? current($domains) : NULL; + } + /** * {@inheritdoc} */ diff --git a/domain_alias/src/Form/DomainAliasDeleteForm.php b/domain_alias/src/Form/DomainAliasDeleteForm.php index 66684688..cd8cc622 100644 --- a/domain_alias/src/Form/DomainAliasDeleteForm.php +++ b/domain_alias/src/Form/DomainAliasDeleteForm.php @@ -2,45 +2,21 @@ namespace Drupal\domain_alias\Form; -use Drupal\Core\Entity\EntityConfirmFormBase; -use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityDeleteForm; use Drupal\Core\Url; /** * Builds the form to delete a domain_alias record. */ -class DomainAliasDeleteForm extends EntityConfirmFormBase { - - /** - * {@inheritdoc} - */ - public function getQuestion() { - return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label())); - } +class DomainAliasDeleteForm extends EntityDeleteForm { /** * {@inheritdoc} */ public function getCancelUrl() { - $arguments['domain'] = $this->entity->getDomainId(); - return new Url('domain_alias.admin', $arguments); - } - - /** - * {@inheritdoc} - */ - public function getConfirmText() { - return $this->t('Delete'); - } - - /** - * {@inheritdoc} - */ - public function submit(array $form, FormStateInterface $form_state) { - $this->entity->delete(); - drupal_set_message($this->t('DomainAlias %label has been deleted.', array('%label' => $this->entity->label()))); - \Drupal::logger('domain_alias')->notice('DomainAlias %label has been deleted.', array('%label' => $this->entity->label())); - $form_state->setRedirectUrl($this->getCancelUrl()); + return new Url('domain_alias.admin', [ + 'domain' => $this->entity->getDomainId(), + ]); } } diff --git a/domain_alias/src/Tests/DomainAliasTestBase.php b/domain_alias/src/Tests/DomainAliasTestBase.php deleted file mode 100644 index cb22c3f7..00000000 --- a/domain_alias/src/Tests/DomainAliasTestBase.php +++ /dev/null @@ -1,63 +0,0 @@ -getHostname(); - } - $values = array( - 'domain_id' => $domain->id(), - 'pattern' => $pattern, - 'redirect' => $redirect, - ); - // Replicate the logic for creating machine_name patterns. - // @see ConfigBase::validate() - $machine_name = strtolower(preg_replace('/[^a-z0-9_]/', '_', $values['pattern'])); - $values['id'] = str_replace(array('*', '.', ':'), '_', $machine_name); - $alias = \Drupal::entityTypeManager()->getStorage('domain_alias')->create($values); - if ($save) { - $alias->save(); - } - - return $alias; - } - -} diff --git a/domain_alias/tests/src/Functional/DomainAliasActionsTest.php b/domain_alias/tests/src/Functional/DomainAliasActionsTest.php new file mode 100644 index 00000000..4b0e9b4b --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasActionsTest.php @@ -0,0 +1,137 @@ +admin_user = $this->drupalCreateUser(['administer domains', 'access administration pages']); + $this->drupalLogin($this->admin_user); + + // Create test domains. + $this->domainCreateTestDomains(3); + + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $alias_loader = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $domains = $domain_storage->loadMultiple(); + + // Save these for later testing. + $original_domains = $domains; + + $base = $this->baseHostname; + $hostnames = [$base, 'one.' . $base, 'two.' . $base]; + + // Our patterns should map to example.com, one.example.com, two.example.com. + $patterns = ['*.' . $base, 'four.' . $base, 'five.' . $base]; + $i = 0; + foreach ($domains as $domain) { + $this->assert($domain->getHostname() == $hostnames[$i], 'Hostnames set correctly'); + $this->assert($domain->getCanonical() == $hostnames[$i], 'Canonical domains set correctly'); + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => array_shift($patterns), + 'redirect' => 0, + 'environment' => 'local', + ]; + $this->createDomainAlias($values); + $i++; + } + + $path = $domain->getScheme() . 'five.' . $base . '/admin/config/domain'; + + // Visit the domain overview administration page. + $this->drupalGet($path); + $this->assertResponse(200); + + // Test the domains. + $domains = $domain_storage->loadMultiple(); + $this->assertCount(3, $domains, 'Three domain records found.'); + + // Check the default domain. + $default = $domain_storage->loadDefaultId(); + $key = 'example_com'; + $this->assertTrue($default == $key, 'Default domain set correctly.'); + + // Test some text on the page. + foreach ($domains as $domain) { + $name = $domain->label(); + $this->assertText($name, 'Name found properly.'); + } + // Test the list of actions. + $actions = ['delete', 'disable', 'default']; + foreach ($actions as $action) { + $this->assertRaw("/domain/{$action}/", 'Actions found properly.'); + } + // Check that all domains are active. + $this->assertNoRaw('Inactive', 'Inactive domain not found.'); + + // Disable a domain and test the enable link. + $this->clickLink('Disable', 0); + $this->assertRaw('Inactive', 'Inactive domain found.'); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + foreach ($domain_storage->loadMultiple() as $domain) { + if ($domain->id() == 'one_example_com') { + $this->assertEmpty($domain->status(), 'One domain inactive.'); + } + else { + $this->assertNotEmpty($domain->status(), 'Other domains active.'); + } + } + + // Test the list of actions. + $actions = ['enable', 'delete', 'disable', 'default']; + foreach ($actions as $action) { + $this->assertRaw("/domain/{$action}/", 'Actions found properly.'); + } + // Re-enable the domain. + $this->clickLink('Enable', 0); + $this->assertNoRaw('Inactive', 'Inactive domain not found.'); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + foreach ($domain_storage->loadMultiple() as $domain) { + $this->assertNotEmpty($domain->status(), 'All domains active.'); + } + + // Set a new default domain. + $this->clickLink('Make default', 0); + + // Visit the domain overview administration page to clear cache. + $this->drupalGet($path); + $this->assertResponse(200); + + // Check the default domain. + $domain_storage->resetCache(); + $default = $domain_storage->loadDefaultId(); + $key = 'one_example_com'; + $this->assertTrue($default == $key, 'Default domain set correctly.'); + + // Did the hostnames change accidentally? + foreach ($domain_storage->loadMultiple() as $id => $domain) { + $this->assertTrue($domain->getHostname() == $original_domains[$id]->getHostname(), 'Hostnames match.'); + } + + } + +} diff --git a/domain_alias/tests/src/Functional/DomainAliasEnvironmentTest.php b/domain_alias/tests/src/Functional/DomainAliasEnvironmentTest.php new file mode 100644 index 00000000..d2cd2f18 --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasEnvironmentTest.php @@ -0,0 +1,95 @@ +domainCreateTestDomains(3); + } + + /** + * Test for environment matching. + */ + public function testDomainAliasEnvironments() { + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $alias_loader = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $domains = $domain_storage->loadMultipleSorted(NULL, TRUE); + // Our patterns should map to example.com, one.example.com, two.example.com. + $patterns = ['*.' . $this->baseHostname, 'four.' . $this->baseHostname, 'five.' . $this->baseHostname]; + foreach ($domains as $domain) { + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => array_shift($patterns), + 'redirect' => 0, + 'environment' => 'local', + ]; + $this->createDomainAlias($values); + } + // Test the environment loader. + $local = $alias_loader->loadByEnvironment('local'); + $this->assert(count($local) == 3, 'Three aliases set to local'); + // Test the environment matcher. $domain here is two.example.com. + $match = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($match) == 1, 'One environment match loaded'); + $alias = current($match); + $this->assert($alias->getPattern() == 'five.' . $this->baseHostname, 'Proper pattern match loaded.'); + + // Set one alias to a different environment. + $alias->set('environment', 'testing')->save(); + $local = $alias_loader->loadByEnvironment('local'); + $this->assert(count($local) == 2, 'Two aliases set to local'); + // Test the environment matcher. $domain here is two.example.com. + $matches = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($matches) == 0, 'No environment matches loaded'); + + // Test the environment matcher. $domain here is one.example.com. + $domain = $domain_storage->load('one_example_com'); + $matches = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($matches) == 1, 'One environment match loaded'); + $alias = current($matches); + $this->assert($alias->getPattern() == 'four.' . $this->baseHostname, 'Proper pattern match loaded.'); + + // Now load a page and check things. + // Since we cannot read the service request, we place a block + // which shows links to all domains. + $this->drupalPlaceBlock('domain_switcher_block'); + + // To get around block access, let the anon user view the block. + user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['administer domains']); + // For a non-aliased request, the url list should be normal. + $this->drupalGet($domain->getPath()); + foreach ($domains as $domain) { + $this->assertSession()->assertEscaped($domain->getHostname()); + $this->assertSession()->linkByHrefExists($domain->getPath(), 0, 'Link found: ' . $domain->getPath()); + } + // For an aliased request (four.example.com), the list should be aliased. + $url = $domain->getScheme() . $alias->getPattern(); + $this->drupalGet($url); + foreach ($matches as $match) { + $this->assertSession()->assertEscaped($match->getPattern()); + } + } + +} diff --git a/domain_alias/tests/src/Functional/DomainAliasListBuilderTest.php b/domain_alias/tests/src/Functional/DomainAliasListBuilderTest.php index fcf01d19..db4e0e23 100644 --- a/domain_alias/tests/src/Functional/DomainAliasListBuilderTest.php +++ b/domain_alias/tests/src/Functional/DomainAliasListBuilderTest.php @@ -2,21 +2,21 @@ namespace Drupal\Tests\domain_alias\Functional; -use Drupal\Tests\domain\Functional\DomainTestBase; +use Drupal\domain\DomainInterface; /** * Tests behavior for the domain list builder. * * @group domain_alias */ -class DomainAliasListBuilderTest extends DomainTestBase { +class DomainAliasListBuilderTest extends DomainAliasTestBase { /** * Modules to enable. * * @var array */ - public static $modules = array('domain', 'domain_alias', 'user'); + public static $modules = ['domain', 'domain_alias', 'user']; /** * {@inheritdoc} @@ -32,7 +32,7 @@ protected function setUp() { * Basic test setup. */ public function testDomainListBuilder() { - $admin = $this->drupalCreateUser(array( + $admin = $this->drupalCreateUser([ 'bypass node access', 'administer content types', 'administer node fields', @@ -40,7 +40,7 @@ public function testDomainListBuilder() { 'administer domains', 'administer domain aliases', 'view domain aliases', - )); + ]); $this->drupalLogin($admin); $this->drupalGet('admin/config/domain'); @@ -53,20 +53,20 @@ public function testDomainListBuilder() { } // Now login as a user with limited rights. - $account = $this->drupalCreateUser(array( + $account = $this->drupalCreateUser([ 'create article content', 'edit any article content', 'edit assigned domains', 'view domain list', 'view domain aliases', 'edit domain aliases', - )); + ]); $ids = ['example_com', 'one_example_com']; - $this->addDomainsToEntity('user', $account->id(), $ids, DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $account->id(), $ids, DomainInterface::DOMAIN_ADMIN_FIELD); $user_storage = \Drupal::entityTypeManager()->getStorage('user'); $user = $user_storage->load($account->id()); $manager = \Drupal::service('domain.element_manager'); - $values = $manager->getFieldValues($user, DOMAIN_ADMIN_FIELD); + $values = $manager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); $this->assert(count($values) == 2, 'User saved with two domain records.'); $this->drupalLogin($account); diff --git a/domain_alias/tests/src/Functional/DomainAliasListHostnameTest.php b/domain_alias/tests/src/Functional/DomainAliasListHostnameTest.php new file mode 100644 index 00000000..1d392f79 --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasListHostnameTest.php @@ -0,0 +1,83 @@ +domainCreateTestDomains(3); + } + + /** + * Test for environment matching. + */ + public function testDomainAliasEnvironments() { + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $alias_loader = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $domains = $domain_storage->loadMultiple(); + + $base = $this->baseHostname; + $hostnames = [$base, 'one.' . $base, 'two.' . $base]; + + // Our patterns should map to example.com, one.example.com, two.example.com. + $patterns = ['*.' . $base, 'four.' . $base, 'five.' . $base]; + $i = 0; + foreach ($domains as $domain) { + $this->assert($domain->getHostname() == $hostnames[$i], 'Hostnames set correctly'); + $this->assert($domain->getCanonical() == $hostnames[$i], 'Canonical domains set correctly'); + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => array_shift($patterns), + 'redirect' => 0, + 'environment' => 'local', + ]; + $this->createDomainAlias($values); + $i++; + } + // Test the environment loader. + $local = $alias_loader->loadByEnvironment('local'); + $this->assert(count($local) == 3, 'Three aliases set to local'); + // Test the environment matcher. $domain here is two.example.com. + $match = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($match) == 1, 'One environment match loaded'); + $alias = current($match); + $this->assert($alias->getPattern() == 'five.' . $base, 'Proper pattern match loaded.'); + + $admin = $this->drupalCreateUser([ + 'bypass node access', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer domains', + ]); + $this->drupalLogin($admin); + + // Load an aliased domain. + $this->drupalGet($domain->getScheme() . 'five.' . $base . '/admin/config/domain'); + $this->assertSession()->statusCodeEquals(200); + + // Save the form. + $this->pressButton('edit-submit'); + // Ensure the values haven't changed. + $i = 0; + $domains = $domain_storage->loadMultiple(); + foreach ($domains as $domain) { + $this->assert($domain->getHostname() == $hostnames[$i], 'Hostnames set correctly'); + $this->assert($domain->getCanonical() == $hostnames[$i], 'Canonical domains set correctly'); + $i++; + } + } + +} diff --git a/domain_alias/tests/src/Functional/DomainAliasMiddlewareTest.php b/domain_alias/tests/src/Functional/DomainAliasMiddlewareTest.php new file mode 100644 index 00000000..e729d222 --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasMiddlewareTest.php @@ -0,0 +1,21 @@ +drupalPlaceBlock('domain_server_block'); // To get around block access, let the anon user view the block. - user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, array('administer domains')); + user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['administer domains']); + + // Set the storage handles. + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $alias_storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); // Set known prefixes that work with our tests. This will give us domains // 'example.com' and 'one.example.com' aliased to 'two.example.com' and @@ -41,7 +45,7 @@ public function testDomainAliasNegotiator() { $prefixes = ['two', 'three']; // Test the response of each home page. /** @var \Drupal\domain\Entity\Domain $domain */ - foreach (\Drupal::service('domain.loader')->loadMultiple() as $domain) { + foreach ($domain_storage->loadMultiple() as $domain) { $alias_domains[] = $domain; $this->drupalGet($domain->getPath()); $this->assertRaw($domain->label(), 'Loaded the proper domain.'); @@ -52,9 +56,9 @@ public function testDomainAliasNegotiator() { foreach ($alias_domains as $index => $alias_domain) { $prefix = $prefixes[$index]; // Set a known pattern. - $pattern = $prefix . '.' . $this->base_hostname; + $pattern = $prefix . '.' . $this->baseHostname; $this->domainAliasCreateTestAlias($alias_domain, $pattern); - $alias = \Drupal::service('domain_alias.loader')->loadByPattern($pattern); + $alias = $alias_storage->loadByPattern($pattern); // Set the URL for the request. Note that this is not saved, it is just // URL generation. $alias_domain->set('hostname', $pattern); @@ -76,13 +80,13 @@ public function testDomainAliasNegotiator() { } // Test a wildcard alias. // @TODO: Refactor this test to merge with the above. - $alias_domain = \Drupal::service('domain.loader')->loadDefaultDomain(); - $pattern = '*.' . $this->base_hostname; + $alias_domain = $domain_storage->loadDefaultDomain(); + $pattern = '*.' . $this->baseHostname; $this->domainAliasCreateTestAlias($alias_domain, $pattern); - $alias = \Drupal::service('domain_alias.loader')->loadByPattern($pattern); + $alias = $alias_storage->loadByPattern($pattern); // Set the URL for the request. Note that this is not saved, it is just // URL generation. - $alias_domain->set('hostname', 'four.' . $this->base_hostname); + $alias_domain->set('hostname', 'four.' . $this->baseHostname); $alias_domain->setPath(); $url = $alias_domain->getPath(); $this->drupalGet($url); @@ -100,7 +104,7 @@ public function testDomainAliasNegotiator() { $this->drupalGet($url); // Revoke the permission change. - user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('administer domains')); + user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, ['administer domains']); } } diff --git a/domain_alias/src/Tests/DomainAliasSortTest.php b/domain_alias/tests/src/Functional/DomainAliasSortTest.php similarity index 84% rename from domain_alias/src/Tests/DomainAliasSortTest.php rename to domain_alias/tests/src/Functional/DomainAliasSortTest.php index a298c191..433be90c 100644 --- a/domain_alias/src/Tests/DomainAliasSortTest.php +++ b/domain_alias/tests/src/Functional/DomainAliasSortTest.php @@ -1,6 +1,6 @@ sortList(); - $loader = \Drupal::service('domain_alias.loader'); + $storage = \Drupal::entityTypeManager()->getStorage('domain_alias'); foreach ($list as $key => $values) { - $patterns = $loader->getPatterns($key); - $this->assertTrue(empty(array_diff($values, $patterns)), 'Pattern matched as expected for ' . $key); + $patterns = $storage->getPatterns($key); + $this->assertEmpty(array_diff($values, $patterns), 'Pattern matched as expected for ' . $key); } - } /** * An array of expected matches to specific domains. */ private function sortList() { - return array( + return [ 'example.com' => [ 'example.com', 'example.*', @@ -77,8 +76,7 @@ private function sortList() { '*.com:8080', '*.com:*', ], - ); + ]; } } - diff --git a/domain_alias/tests/src/Functional/DomainAliasTestBase.php b/domain_alias/tests/src/Functional/DomainAliasTestBase.php new file mode 100644 index 00000000..a7af96f7 --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasTestBase.php @@ -0,0 +1,22 @@ +loadByHostname($key); - $this->assertTrue(!empty($domain), 'Test domain created.'); + $domain = \Drupal::entityTypeManager()->getStorage('domain')->loadByHostname($key); + $this->assertNotEmpty($domain, 'Test domain created.'); // Valid patterns to test. Valid is the boolean value. $patterns = [ 'localhost' => 1, 'example.com' => 1, - 'www.example.com' => 1, // see www-prefix test, below. + // See www-prefix test, below. + 'www.example.com' => 1, '*.example.com' => 1, 'one.example.com' => 1, 'example.com:8080' => 1, - '*.*.example.com' => 0, // only one wildcard. - 'example.com::8080' => 0, // only one colon. - 'example.com:abc' => 0, // no letters after a colon. - '.example.com' => 0, // cannot begin with a dot. - 'example.com.' => 0, // cannot end with a dot. - 'EXAMPLE.com' => 0, // lowercase only. - 'éxample.com' => 0, // ascii-only. - 'foo.com' => 0, // duplicate. + // Must have a dot or be localhost. + 'foobar' => 0, + // Only one wildcard. + '*.*.example.com' => 0, + // Only one colon. + 'example.com::8080' => 0, + // No letters after a colon. + 'example.com:abc' => 0, + // Cannot begin with a dot. + '.example.com' => 0, + // Cannot end with a dot. + 'example.com.' => 0, + // Lowercase only. + 'EXAMPLE.com' => 0, + // ascii-only. + 'éxample.com' => 0, + // duplicate. + 'foo.com' => 0, ]; foreach ($patterns as $pattern => $valid) { - $alias = $this->domainAliasCreateTestAlias($domain, $pattern, 0, FALSE); + $alias = $this->domainAliasCreateTestAlias($domain, $pattern, 0, 'default', FALSE); $errors = $validator->validate($alias); if ($valid) { - $this->assertTrue(empty($errors), new FormattableMarkup('Validation test for @pattern passed.', array('@pattern' => $pattern))); + $this->assertEmpty($errors, 'Validation test success.'); } else { - $this->assertTrue(!empty($errors), new FormattableMarkup('Validation test for @pattern failed.', array('@pattern' => $pattern))); + $this->assertNotEmpty($errors, 'Validation test success.'); } } // Test the configurable option. $config = $this->config('domain.settings'); - $config->set('allow_non_ascii', true)->save(); + $config->set('allow_non_ascii', TRUE)->save(); // Valid hostnames to test. Valid is the boolean value. $patterns = [ - 'éxample.com' => 1, // ascii-only allowed. + // ascii-only allowed. + 'éxample.com' => 1, ]; foreach ($patterns as $pattern => $valid) { - $alias = $this->domainAliasCreateTestAlias($domain, $pattern, 0, FALSE); + $alias = $this->domainAliasCreateTestAlias($domain, $pattern, 0, 'default', FALSE); $errors = $validator->validate($alias); if ($valid) { - $this->assertTrue(empty($errors), new FormattableMarkup('Validation test for @pattern passed.', array('@pattern' => $pattern))); + $this->assertEmpty($errors, 'Validation test success.'); } else { - $this->assertTrue(!empty($errors), new FormattableMarkup('Validation test for @pattern failed.', array('@pattern' => $pattern))); + $this->assertNotEmpty($errors, 'Validation test success.'); } } - } } diff --git a/domain_alias/tests/src/Functional/DomainAliasWildcardTest.php b/domain_alias/tests/src/Functional/DomainAliasWildcardTest.php new file mode 100644 index 00000000..1a04a34b --- /dev/null +++ b/domain_alias/tests/src/Functional/DomainAliasWildcardTest.php @@ -0,0 +1,87 @@ +domainCreateTestDomains(3); + } + + /** + * Test for environment matching. + */ + public function testDomainAliasWildcards() { + $domain_storage = \Drupal::entityTypeManager()->getStorage('domain'); + $alias_loader = \Drupal::entityTypeManager()->getStorage('domain_alias'); + $domains = $domain_storage->loadMultipleSorted(NULL, TRUE); + // Our patterns should map to example.com, one.example.com, two.example.com. + $patterns = ['example.*', 'four.example.*', 'five.example.*']; + foreach ($domains as $domain) { + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => array_shift($patterns), + 'redirect' => 0, + 'environment' => 'local', + ]; + $this->createDomainAlias($values); + } + // Test the environment loader. + $local = $alias_loader->loadByEnvironment('local'); + $this->assert(count($local) == 3, 'Three aliases set to local'); + // Test the environment matcher. $domain here is two.example.com. + $match = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($match) == 1, 'One environment match loaded'); + $alias = current($match); + $this->assert($alias->getPattern() == 'five.example.*', 'Proper pattern match loaded.'); + + // Test the environment matcher. $domain here is one.example.com. + $domain = $domain_storage->load('one_example_com'); + $matches = $alias_loader->loadByEnvironmentMatch($domain, 'local'); + $this->assert(count($matches) == 1, 'One environment match loaded'); + $alias = current($matches); + $this->assert($alias->getPattern() == 'four.example.*', 'Proper pattern match loaded.'); + + // Now load a page and check things. + // Since we cannot read the service request, we place a block + // which shows links to all domains. + $this->drupalPlaceBlock('domain_switcher_block'); + + // To get around block access, let the anon user view the block. + user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['administer domains']); + // For a non-aliased request, the url list should be normal. + $this->drupalGet($domain->getPath()); + foreach ($domains as $domain) { + $this->assertSession()->assertEscaped($domain->getHostname()); + $this->assertSession()->linkByHrefExists($domain->getPath(), 0, 'Link found: ' . $domain->getPath()); + } + // For an aliased request (four.example.com), the list should be aliased. + $url = $domain->getScheme() . str_replace('*', $this->baseTLD, $alias->getPattern()); + $this->drupalGet($url); + foreach ($matches as $match) { + $this->assertSession()->assertEscaped(str_replace('*', $this->baseTLD, $match->getPattern())); + } + } + +} diff --git a/domain_alias/tests/src/Kernel/DomainAliasDomainDeleteTest.php b/domain_alias/tests/src/Kernel/DomainAliasDomainDeleteTest.php new file mode 100644 index 00000000..cdcf2635 --- /dev/null +++ b/domain_alias/tests/src/Kernel/DomainAliasDomainDeleteTest.php @@ -0,0 +1,86 @@ +domainCreateTestDomains(2); + + // Get the services. + $this->domainStorage = \Drupal::entityTypeManager()->getStorage('domain'); + $this->aliasStorage = \Drupal::entityTypeManager()->getStorage('domain_alias'); + } + + /** + * Tests alias deletion on domain deletion. + */ + public function testDomainDelete() { + $domains = $this->domainStorage->loadMultiple(); + $patterns = [ + 'example_com' => '*.example.com', + 'one_example_com' => 'foo.example.com', + ]; + + // Create an alias. + foreach ($domains as $id => $domain) { + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => $patterns[$id], + 'redirect' => 0, + 'environment' => 'local', + ]; + $this->createDomainAlias($values); + $alias = $this->aliasStorage->loadByPattern($patterns[$id]); + $this->assertNotEmpty($alias, 'Alias saved properly'); + } + + // Delete one domain. + $domain->delete(); + $alias = $this->aliasStorage->loadByPattern($patterns[$id]); + $this->assertEmpty($alias, 'Alias deleted properly'); + + // Check the remaining domain, which should still have an alias. + $domain = $this->domainStorage->load('example_com'); + $alias = $this->aliasStorage->loadByPattern($patterns[$domain->id()]); + $this->assertNotEmpty($alias, 'Alias retained properly'); + } + +} diff --git a/domain_alias/tests/src/Traits/DomainAliasTestTrait.php b/domain_alias/tests/src/Traits/DomainAliasTestTrait.php new file mode 100644 index 00000000..bc7e1fff --- /dev/null +++ b/domain_alias/tests/src/Traits/DomainAliasTestTrait.php @@ -0,0 +1,67 @@ +getStorage('domain_alias')->create($values); + if ($save) { + $alias->save(); + } + + return $alias; + } + + /** + * Creates an alias for testing without passing values. + * + * @param \Drupal\domain\DomainInterface $domain + * A domain entity. + * @param string $pattern + * An optional alias pattern. + * @param int $redirect + * An optional redirect (301 or 302). + * @param int $environment + * An optional environment string. + * @param bool $save + * Whether to save the alias or return for validation. + * + * @return \Drupal\domain_alias\Entity\DomainAlias + * A domain alias entity. + */ + public function domainAliasCreateTestAlias(DomainInterface $domain, $pattern = NULL, $redirect = 0, $environment = 'default', $save = TRUE) { + if (empty($pattern)) { + $pattern = '*.' . $domain->getHostname(); + } + $values = [ + 'domain_id' => $domain->id(), + 'pattern' => $pattern, + 'redirect' => $redirect, + 'environment' => $environment, + ]; + + return $this->createDomainAlias($values, $save); + } + +} diff --git a/domain_alpha/domain_alpha.info.yml b/domain_alpha/domain_alpha.info.yml deleted file mode 100644 index 8485b51e..00000000 --- a/domain_alpha/domain_alpha.info.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: Domain Alpha Updates -description: 'Provides per-release updates for critical changes.' -type: module -package: Domain -version: VERSION -core: 8.x -dependencies: - - domain diff --git a/domain_alpha/domain_alpha.install b/domain_alpha/domain_alpha.install deleted file mode 100644 index 66ef0b1d..00000000 --- a/domain_alpha/domain_alpha.install +++ /dev/null @@ -1,71 +0,0 @@ -getStorage('domain')->loadMultiple(); - foreach ($domains as $domain) { - /** @var $domain \Drupal\domain\Entity\Domain */ - // Existing id. - $id = $domain->getDomainId(); - // New id. - $domain->createDomainId(); - $new_id = $domain->getDomainId(); - // Check to see if this update is needed. - if ($id != $new_id) { - $domain->save(); - $rebuild = TRUE; - } - } - if ($rebuild) { - // Trigger permissions rebuild action. - node_access_needs_rebuild(TRUE); - } -} - -/** - * Provide a new update for 8001, for users who never ran 8001. - * - * See https://github.com/agentrickard/domain/issues/310. - */ -function domain_alpha_update_8002(&$sandbox) { - domain_alpha_update_8001($sandbox); -} - -/** - * Set the Domain Admin field to use the proper plugin. - */ -function domain_alpha_update_8003(&$sandbox) { - $id = 'user.user.field_domain_admin'; - $storage = \Drupal::entityTypeManager()->getStorage('field_config'); - if ($field = $storage->load($id)) { - $new_field = $field->toArray(); - if ($new_field['settings']['handler'] != 'domain:domain') { - $new_field['settings']['handler'] = 'domain:domain'; - $field_config = $storage->create($new_field); - $field_config->original = $field; - $field_config->enforceIsNew(FALSE); - $field_config->save(); - } - } -} diff --git a/domain_config/README.md b/domain_config/README.md index 5e9b4806..9b5138b5 100644 --- a/domain_config/README.md +++ b/domain_config/README.md @@ -45,9 +45,9 @@ langcode: en default_langcode: en ``` -An override file should contain only the specific data that you want to override. -To override the name, the only line needed is the one beginning ```name:```. -The resulting override looks like this: +An override file should contain only the specific data that you want to +override. To override the name, the only line needed is the one beginning + ```name:```. The resulting override looks like this: ```YAML name: Three @@ -84,13 +84,23 @@ settings.php overrides ---------------------- For environment-specific or sensitive overrides, use the settings.php method. -In the above case, add -`$conf['domain.config.three_example_com.en.system.site']['name'] = "My special site";` +In the above case, add: + +`$config['domain.config.three_example_com.en.system.site']['name'] = "My special site";` + to your local settings.php file. This will ensure that the `three.example.com` -domain gets the correct value regardless of other module overrides. +domain gets the correct value regardless of other module overrides. If you do +not have a language set the config system will fallback on the domain-specific +setting (note the missing '.en'): + +`$config['domain.config.three_example_com.system.site']['name'] = "My special site";` + +To set nested values you need to nest the config array: + +`$config['domain.config.three_example_com.system.site']['page']['front'] = '/node/1;` -Read more about the "Configuration override system" -at https://www.drupal.org/node/1928898. +Read more about the "Configuration override system" at +https://www.drupal.org/node/1928898. Installation ============ diff --git a/domain_config/domain_config.info.yml b/domain_config/domain_config.info.yml index f286a5c5..219276ee 100644 --- a/domain_config/domain_config.info.yml +++ b/domain_config/domain_config.info.yml @@ -2,7 +2,12 @@ name: Domain Configuration description: 'Allows domain specific configuration.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - domain + - domain:domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_config/domain_config.services.yml b/domain_config/domain_config.services.yml index c019355f..1e1029f9 100644 --- a/domain_config/domain_config.services.yml +++ b/domain_config/domain_config.services.yml @@ -3,5 +3,13 @@ services: class: Drupal\domain_config\DomainConfigOverrider tags: - { name: config.factory.override, priority: -150} - arguments: ['@domain.negotiator', '@config.storage'] + arguments: ['@config.storage', '@module_handler'] + domain_config.library.discovery.collector: + decorates: library.discovery.collector + class: \Drupal\domain_config\DomainConfigLibraryDiscoveryCollector + arguments: ['@cache.discovery', '@lock', '@library.discovery.parser', '@theme.manager'] + tags: + - { name: needs_destruction } + calls: + - [setDomainNegotiator, ['@domain.negotiator']] diff --git a/domain_config/src/DomainConfigLibraryDiscoveryCollector.php b/domain_config/src/DomainConfigLibraryDiscoveryCollector.php new file mode 100644 index 00000000..2f320be8 --- /dev/null +++ b/domain_config/src/DomainConfigLibraryDiscoveryCollector.php @@ -0,0 +1,47 @@ +domain = $domainNegotiator->getActiveDomain(); + } + + /** + * {@inheritdoc} + */ + protected function getCid() { + if (!isset($this->cid)) { + $domain_id = 'null'; + if (!empty($this->domain)) { + $domain_id = $this->domain->id(); + } + $this->cid = 'library_info:' . $domain_id . ':' . $this->themeManager->getActiveTheme()->getName(); + } + + return $this->cid; + } + +} diff --git a/domain_config/src/DomainConfigOverrider.php b/domain_config/src/DomainConfigOverrider.php index 8c55b6fb..5fe25c80 100644 --- a/domain_config/src/DomainConfigOverrider.php +++ b/domain_config/src/DomainConfigOverrider.php @@ -3,10 +3,10 @@ namespace Drupal\domain_config; use Drupal\domain\DomainInterface; -use Drupal\domain\DomainNegotiatorInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryOverrideInterface; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; /** * Domain-specific config overrides. @@ -30,17 +30,24 @@ class DomainConfigOverrider implements ConfigFactoryOverrideInterface { */ protected $storage; + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + /** * The domain context of the request. * - * @var \Drupal\domain\DomainInterface $domain + * @var \Drupal\domain\DomainInterface */ protected $domain; /** * The language context of the request. * - * @var \Drupal\Core\Language\LanguageInterface $language + * @var \Drupal\Core\Language\LanguageInterface */ protected $language; @@ -53,64 +60,110 @@ class DomainConfigOverrider implements ConfigFactoryOverrideInterface { */ protected $languageManager; + /** + * Indicates that the request context is set. + * + * @var bool + */ + protected $contextSet; + /** * Constructs a DomainConfigSubscriber object. * - * @param \Drupal\domain\DomainNegotiatorInterface $negotiator - * The domain negotiator service. * @param \Drupal\Core\Config\StorageInterface $storage * The configuration storage engine. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(DomainNegotiatorInterface $negotiator, StorageInterface $storage) { - $this->domainNegotiator = $negotiator; + public function __construct(StorageInterface $storage, ModuleHandlerInterface $module_handler) { $this->storage = $storage; + $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public function loadOverrides($names) { - $overrides = array(); - // loadOverrides() runs on config entities, which means that if we try - // to run this routine on our own data, then we end up in an infinite loop. - // So ensure that we are _not_ looking up a domain.record.*. - $check = current($names); - $list = explode('.', $check); - if (isset($list[0]) && isset($list[1]) && $list[0] == 'domain' && $list[1] == 'record') { - return $overrides; + // Assume first time loading is NULL. + static $load = NULL; + + $config_override_exists = FALSE; + if (is_null($load)) { + // Ensure we don't have any values in settings.php. + $config_from_settings = array_keys($GLOBALS['config']); + foreach ($config_from_settings as $config_key) { + if (strpos($config_key, 'domain.config', 0) !== FALSE) { + $config_override_exists = TRUE; + break; + } + } } - if (empty($this->domain)) { - $this->initiateContext(); + + // Check if any overridden config exists or if we have already + // made this check. + // See https://www.drupal.org/project/domain/issues/3126532. + if ($load === FALSE || (!$this->storage->listAll('domain.config.') && !$config_override_exists)) { + $load = FALSE; + return []; } - if (!empty($this->domain)) { - foreach ($names as $name) { - $config_name = $this->getDomainConfigName($name, $this->domain); - // Check to see if the config storage has an appropriately named file - // containing override data. - if ($override = $this->storage->read($config_name['langcode'])) { - $overrides[$name] = $override; - } - // Check to see if we have a file without a specific language. - elseif ($override = $this->storage->read($config_name['domain'])) { - $overrides[$name] = $override; - } + else { + // Try to prevent repeating lookups. + static $lookups; + // Key should be a known length, so hash. + $key = md5(implode(':', $names)); + if (isset($lookups[$key])) { + return $lookups[$key]; + } - // Apply any settings.php overrides. - if (isset($GLOBALS['config'][$config_name['langcode']])) { - $overrides[$name] = $GLOBALS['config'][$config_name['langcode']]; - } - elseif (isset($GLOBALS['config'][$config_name['domain']])) { - $overrides[$name] = $GLOBALS['config'][$config_name['domain']]; + // Set the context of the override request. + if (empty($this->contextSet)) { + $this->initiateContext(); + } + + // Prepare our overrides. + $overrides = []; + // loadOverrides() runs on config entities, which means that if we try + // to run this routine on our own data, then we end up in an infinite loop. + // So ensure that we are _not_ looking up a domain.record.*. + $check = current($names); + $list = explode('.', $check); + if (isset($list[0]) && isset($list[1]) && $list[0] == 'domain' && $list[1] == 'record') { + $lookups[$key] = $overrides; + return $overrides; + } + if (!empty($this->domain)) { + foreach ($names as $name) { + $config_name = $this->getDomainConfigName($name, $this->domain); + // Check to see if the config storage has an appropriately named file + // containing override data. + if ($override = $this->storage->read($config_name['langcode'])) { + $overrides[$name] = $override; + } + // Check to see if we have a file without a specific language. + elseif ($override = $this->storage->read($config_name['domain'])) { + $overrides[$name] = $override; + } + + // Apply any settings.php overrides. + if (isset($GLOBALS['config'][$config_name['langcode']])) { + $overrides[$name] = $GLOBALS['config'][$config_name['langcode']]; + } + elseif (isset($GLOBALS['config'][$config_name['domain']])) { + $overrides[$name] = $GLOBALS['config'][$config_name['domain']]; + } } + $lookups[$key] = $overrides; } + + return $overrides; } - return $overrides; } /** * Get configuration name for this hostname. * * It will be the same name with a prefix depending on domain and language: + * * @code domain.config.DOMAIN_ID.LANGCODE @endcode * * @param string $name @@ -148,7 +201,9 @@ public function createConfigObject($name, $collection = StorageInterface::DEFAUL * {@inheritdoc} */ public function getCacheableMetadata($name) { - $this->initiateContext(); + if (empty($this->contextSet)) { + $this->initiateContext(); + } $metadata = new CacheableMetadata(); if (!empty($this->domain)) { $metadata->addCacheContexts(['url.site', 'languages:language_interface']); @@ -165,16 +220,20 @@ public function getCacheableMetadata($name) { protected function initiateContext() { // Prevent infinite lookups by caching the request. Since the _construct() // is called for each lookup, this is more efficient. - static $context; - if ($context) { - return; - } - $context++; + $this->contextSet = TRUE; + + // We must ensure that modules have loaded, which they may not have. + // See https://www.drupal.org/project/domain/issues/3025541. + $this->moduleHandler->loadAll(); + // Get the language context. Note that injecting the language manager // into the service created a circular dependency error, so we load from // the core service manager. $this->languageManager = \Drupal::languageManager(); $this->language = $this->languageManager->getCurrentLanguage(); + + // The same issue is true for the domainNegotiator. + $this->domainNegotiator = \Drupal::service('domain.negotiator'); // Get the domain context. $this->domain = $this->domainNegotiator->getActiveDomain(TRUE); } diff --git a/domain_config/src/Routing/DomainRouteProvider.php b/domain_config/src/Routing/DomainRouteProvider.php deleted file mode 100644 index e0c623a6..00000000 --- a/domain_config/src/Routing/DomainRouteProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -getHost() . ':' . $request->getPathInfo() . ':' . $request->getQueryString(); - if ($cached = $this->cache->get($cid)) { - $this->currentPath->setPath($cached->data['path'], $request); - $request->query->replace($cached->data['query']); - return $cached->data['routes']; - } - else { - // Just trim on the right side. - $path = $request->getPathInfo(); - $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); - $path = $this->pathProcessor->processInbound($path, $request); - $this->currentPath->setPath($path, $request); - // Incoming path processors may also set query parameters. - $query_parameters = $request->query->all(); - $routes = $this->getRoutesByPath(rtrim($path, '/')); - $cache_value = [ - 'path' => $path, - 'query' => $query_parameters, - 'routes' => $routes, - ]; - $this->cache->set($cid, $cache_value, CacheBackendInterface::CACHE_PERMANENT, ['route_match']); - return $routes; - } - } - -} diff --git a/domain_config/src/Tests/DomainConfigHomepageTest.php b/domain_config/src/Tests/DomainConfigHomepageTest.php deleted file mode 100644 index 8ae4178e..00000000 --- a/domain_config/src/Tests/DomainConfigHomepageTest.php +++ /dev/null @@ -1,71 +0,0 @@ -config('system.site'); - $site_config->set('page.front', '/node')->save(); - - // No domains should exist. - $this->domainTableIsEmpty(); - // Create four new domains programmatically. - $this->domainCreateTestDomains(5); - // Get the domain list. - $domains = \Drupal::service('domain.loader')->loadMultiple(); - $this->drupalCreateNode(array( - 'type' => 'article', - 'title' => 'Node 1', - 'promoted' => TRUE, - )); - $this->drupalCreateNode(array( - 'type' => 'article', - 'title' => 'Node 2', - 'promoted' => TRUE, - )); - $homepages = $this->getHomepages(); - foreach ($domains as $domain) { - $home = $this->drupalGet($domain->getPath()); - - // Check if this setting is picked up. - $expected = $domain->getPath() . $homepages[$domain->id()]; - $expected_home = $this->drupalGet($expected); - - $this->assertTrue($home == $expected_home, 'Proper home page loaded (' . $domain->id() . ').'); - } - } - - /** - * Returns the expected homepage paths for each domain. - */ - private function getHomepages() { - $homepages = array( - 'example_com' => 'node', - 'one_example_com' => 'node/1', - 'two_example_com' => 'node', - 'three_example_com' => 'node', - 'four_example_com' => 'node/2', - ); - return $homepages; - } - -} diff --git a/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.info.yml b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.info.yml new file mode 100644 index 00000000..25d94b8e --- /dev/null +++ b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.info.yml @@ -0,0 +1,23 @@ +name: "Domain config hook test" +description: "Support module for domain config testing." +type: module +package: Testing +# version: VERSION +core_version_requirement: ^8 || ^9 +hidden: TRUE + +dependencies: [] + +# This module represents several services that could be provided by multiple modules in the Drupal community. +# The following are not playing nice together: +# - A page cache policy service that uses the config factory. +# - A module that implements hook_module_implements. +# - A random hook that hook_module_implements wishes to control. +# - The domain_config module (and domain negotiator service, I believe). + +# When this module is functioning correctly, when a user logs in, there will not be a state key set. + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.module b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.module new file mode 100644 index 00000000..dfbfcf56 --- /dev/null +++ b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.module @@ -0,0 +1,23 @@ +set('domain_config_test__user_login', TRUE); +} + +/** + * Implements hook_module_implements_alter(). + */ +function domain_config_hook_test_module_implements_alter(&$implementations, $hook) { + if ($hook == 'user_login') { + // Turn off the domain_config_hook_test's hook_user_login (above). + unset($implementations['domain_config_hook_test']); + } +} diff --git a/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.services.yml b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.services.yml new file mode 100644 index 00000000..8cb02a0d --- /dev/null +++ b/domain_config/tests/modules/domain_config_hook_test/domain_config_hook_test.services.yml @@ -0,0 +1,7 @@ +services: + domain_config_service.page_cache_request_policy: + class: Drupal\domain_config_hook_test\PageCache\RequestPolicy\PageCacheRequestPolicy + arguments: ['@config.factory'] + tags: + - { name: page_cache_request_policy } + diff --git a/domain_config/tests/modules/domain_config_hook_test/src/PageCache/RequestPolicy/PageCacheRequestPolicy.php b/domain_config/tests/modules/domain_config_hook_test/src/PageCache/RequestPolicy/PageCacheRequestPolicy.php new file mode 100644 index 00000000..6d0cc4b4 --- /dev/null +++ b/domain_config/tests/modules/domain_config_hook_test/src/PageCache/RequestPolicy/PageCacheRequestPolicy.php @@ -0,0 +1,47 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function check(Request $request) { + // This line is important. You have to use this service for it to fail. + $this->configFactory + ->get('system.site'); + + return NULL; + } + +} diff --git a/domain_config/tests/modules/domain_config_middleware_test/config/install/domain_config_middleware_test.settings.yml b/domain_config/tests/modules/domain_config_middleware_test/config/install/domain_config_middleware_test.settings.yml new file mode 100644 index 00000000..03c0bfdd --- /dev/null +++ b/domain_config/tests/modules/domain_config_middleware_test/config/install/domain_config_middleware_test.settings.yml @@ -0,0 +1 @@ +test_setting: false diff --git a/domain_config/tests/modules/domain_config_middleware_test/config/schema/domain_config_middleware_test.schema.yml b/domain_config/tests/modules/domain_config_middleware_test/config/schema/domain_config_middleware_test.schema.yml new file mode 100644 index 00000000..7fddd236 --- /dev/null +++ b/domain_config/tests/modules/domain_config_middleware_test/config/schema/domain_config_middleware_test.schema.yml @@ -0,0 +1,8 @@ +# Schema for the configuration files of the Domain module. +domain_config_middleware_test.settings: + type: config_object + label: 'Domain config middleware settings' + mapping: + test_setting: + type: boolean + label: 'A test setting' diff --git a/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.info.yml b/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.info.yml new file mode 100644 index 00000000..3aeefb04 --- /dev/null +++ b/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.info.yml @@ -0,0 +1,16 @@ +name: "Domain config middleware module tests" +description: "Support module for domain config middleware response testing." +type: module +package: Testing +# version: VERSION +core_version_requirement: ^8 || ^9 +hidden: TRUE + +dependencies: + - domain + - domain_config + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.services.yml b/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.services.yml new file mode 100644 index 00000000..ecaa375c --- /dev/null +++ b/domain_config/tests/modules/domain_config_middleware_test/domain_config_middleware_test.services.yml @@ -0,0 +1,8 @@ +services: + domain_config_test.middleware: + class: Drupal\domain_config_middleware_test\Middleware + arguments: ['@config.factory'] + tags: + # Ensure to come before page caching, so you don't serve cached pages to + # banned users. + - { name: http_middleware, priority: 250 } diff --git a/domain_config/tests/modules/domain_config_middleware_test/src/Middleware.php b/domain_config/tests/modules/domain_config_middleware_test/src/Middleware.php new file mode 100644 index 00000000..dd850098 --- /dev/null +++ b/domain_config/tests/modules/domain_config_middleware_test/src/Middleware.php @@ -0,0 +1,51 @@ +httpKernel = $http_kernel; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { + // This line should break hooks in our code. + // @see https://www.drupal.org/node/2896434. + $config = $this->configFactory->get('domain_config_middleware_test.settings'); + return $this->httpKernel->handle($request, $type, $catch); + } + +} diff --git a/domain_config/tests/modules/domain_config_test/config/install/domain.config.three_example_com.en.system.site.yml b/domain_config/tests/modules/domain_config_test/config/install/domain.config.three_example_com.en.system.site.yml index 5588e1dd..4c7e58f3 100644 --- a/domain_config/tests/modules/domain_config_test/config/install/domain.config.three_example_com.en.system.site.yml +++ b/domain_config/tests/modules/domain_config_test/config/install/domain.config.three_example_com.en.system.site.yml @@ -1,4 +1,3 @@ -name: 'Three' mail: admin@example.com slogan: '' page: diff --git a/domain_config/tests/modules/domain_config_test/domain_config_test.info.yml b/domain_config/tests/modules/domain_config_test/domain_config_test.info.yml index 6d71ad2c..fd2a31c0 100644 --- a/domain_config/tests/modules/domain_config_test/domain_config_test.info.yml +++ b/domain_config/tests/modules/domain_config_test/domain_config_test.info.yml @@ -2,10 +2,15 @@ name: "Domain config module tests" description: "Support module for domain config testing." type: module package: Testing -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 hidden: TRUE dependencies: - domain - domain_config + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_config/tests/modules/domain_config_test/domain_config_test.module b/domain_config/tests/modules/domain_config_test/domain_config_test.module index 88462d78..c78ed6d9 100644 --- a/domain_config/tests/modules/domain_config_test/domain_config_test.module +++ b/domain_config/tests/modules/domain_config_test/domain_config_test.module @@ -2,5 +2,29 @@ /** * @file - * Domain config test module. + * Hook implementations for this module. */ + +use Drupal\domain\DomainInterface; + +/** + * Implements hook_domain_request_alter(). + */ +function domain_config_test_domain_request_alter(DomainInterface $domain) { + $domain->addProperty('config_test', 'aye'); +} + +/** + * Implements hook_page_attachments_alter(). + */ +function domain_config_test_page_attachments_alter(array &$attachments) { + /** @var \Drupal\domain\DomainNegotiatorInterface $Negotiator */ + $negotiator = \Drupal::service('domain.negotiator'); + $domain = $negotiator->getActiveDomain(); + if (!empty($domain) && $domain->get('config_test') == 'aye') { + $attachments['#attached']['http_header'][] = [ + 'X-Domain-Config-Test-page-attachments-hook', + 'invoked', + ]; + } +} diff --git a/domain_config/tests/src/Functional/DomainConfigAlterHookTest.php b/domain_config/tests/src/Functional/DomainConfigAlterHookTest.php new file mode 100644 index 00000000..b2b77338 --- /dev/null +++ b/domain_config/tests/src/Functional/DomainConfigAlterHookTest.php @@ -0,0 +1,74 @@ +domainCreateTestDomains(); + + // Get the services. + $this->negotiator = \Drupal::service('domain.negotiator'); + $this->moduleHandler = \Drupal::service('module_handler'); + } + + /** + * Tests domain request alteration. + */ + public function testHookDomainRequestAlter() { + // Check for the count of hook implementations. + $hooks = $this->moduleHandler->getImplementations('domain_request_alter'); + $this->assertCount(1, $hooks, 'One hook implementation found.'); + + // Assert that the hook is also called on a request with a HTTP Middleware + // that requests config thus triggering an early hook invocation (before + // modules are loaded by the kernel). + $this->drupalGet(''); + $this->assertEquals('invoked', $this->drupalGetHeader('X-Domain-Config-Test-page-attachments-hook')); + } + +} diff --git a/domain_config/tests/src/Functional/DomainConfigCacheTest.php b/domain_config/tests/src/Functional/DomainConfigCacheTest.php new file mode 100644 index 00000000..a5d2bb0b --- /dev/null +++ b/domain_config/tests/src/Functional/DomainConfigCacheTest.php @@ -0,0 +1,78 @@ +domainTableIsEmpty(); + + // Create a new domain programmatically. + $this->domainCreateTestDomains(5); + $expected = []; + + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath()); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . ':'; + } + + $database = \Drupal::database(); + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), 'Cache returns as expected.'); + + // Now create a node and test the cache. + // Create an article node assigned to two domains. + $ids = ['example_com', 'four_example_com']; + $node1 = $this->drupalCreateNode([ + 'type' => 'article', + 'field_domain_access' => [$ids], + 'path' => '/test' + ]); + + $original = $expected; + + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath() . 'test'); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . 'test:'; + } + + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), 'Cache returns as expected.'); + + // When we delete the node, we want all cids removed. + $node1->delete(); + + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($original), sort($result), 'Cache returns as expected.'); + + } + +} diff --git a/domain_config/tests/src/Functional/DomainConfigHomepageTest.php b/domain_config/tests/src/Functional/DomainConfigHomepageTest.php new file mode 100644 index 00000000..9fd78a57 --- /dev/null +++ b/domain_config/tests/src/Functional/DomainConfigHomepageTest.php @@ -0,0 +1,109 @@ +config('system.site'); + $site_config->set('page.front', '/node')->save(); + + // No domains should exist. + $this->domainTableIsEmpty(); + // Create four new domains programmatically. + $this->domainCreateTestDomains(5); + // Get the domain list. + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $node1 = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => 'Node 1', + 'promoted' => TRUE, + ]); + $node2 = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => 'Node 2', + 'promoted' => TRUE, + ]); + $node3 = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => 'Node 3', + 'promoted' => TRUE, + ]); + $homepages = $this->getHomepages(); + foreach ($domains as $domain) { + foreach (['en', 'es'] as $langcode) { + $prefix = ''; + if ($langcode == 'es') { + $prefix = 'es/'; + } + $home = $this->drupalGet($domain->getPath() . $prefix); + + // Check if this setting is picked up. + $expected = $domain->getPath() . $prefix . $homepages[$domain->id()][$langcode]; + $expected_home = $this->drupalGet($expected); + + $this->assertEqual($home, $expected_home, 'Proper home page loaded (' . $domain->id() . ').'); + } + } + // Explicit test for https://www.drupal.org/project/domain/issues/3154402 + // Create and login user. + $admin_user = $this->drupalCreateUser(['bypass node access', 'access administration pages']); + $this->drupalLogin($admin_user); + $this->drupalGet($domain->getPath() . 'node/' . $node3->id() . '/delete'); + $this->getSession()->getPage()->pressButton('Delete'); + $this->drupalLogout(); + + // Retest the homepages. + foreach ($domains as $domain) { + foreach (['en', 'es'] as $langcode) { + $prefix = ''; + if ($langcode == 'es') { + $prefix = 'es/'; + } + // Prime the cache to prevent a bigpipe mismatch. + $this->drupalGet($domain->getPath() . $prefix); + $home = $this->drupalGet($domain->getPath() . $prefix); + + // Check if this setting is picked up. + $expected = $domain->getPath() . $prefix . $homepages[$domain->id()][$langcode]; + $expected_home = $this->drupalGet($expected); + + $this->assertEqual($home, $expected_home, 'Proper home page loaded (' . $domain->id() . ').'); + } + } + } + + /** + * Returns the expected homepage paths for each domain. + */ + private function getHomepages() { + $homepages = [ + 'example_com' => ['en' => 'node', 'es' => 'node'], + 'one_example_com' => ['en' => 'node/1', 'es' => 'node'], + 'two_example_com' => ['en' => 'node', 'es' => 'node'], + 'three_example_com' => ['en' => 'node', 'es' => 'node'], + 'four_example_com' => ['en' => 'node/2', 'es' => 'node/2'], + ]; + return $homepages; + } + +} diff --git a/domain_config/tests/src/Functional/DomainConfigHookProblemTest.php b/domain_config/tests/src/Functional/DomainConfigHookProblemTest.php new file mode 100644 index 00000000..c6fb7a11 --- /dev/null +++ b/domain_config/tests/src/Functional/DomainConfigHookProblemTest.php @@ -0,0 +1,30 @@ +drupalGet('user/login'); + $user = $this->drupalCreateUser([]); + $edit = ['name' => $user->getAccountName(), 'pass' => $user->passRaw]; + $this->submitForm($edit, 'Log in'); + + $test = \Drupal::state()->get('domain_config_test__user_login', NULL); + // When this test passes, it means domain_config_hook_test_user_login was + // not run. + $this->assertNull($test, 'The hook_user_login state message is set.'); + } + +} diff --git a/domain_config/src/Tests/DomainConfigOverriderTest.php b/domain_config/tests/src/Functional/DomainConfigOverriderTest.php similarity index 76% rename from domain_config/src/Tests/DomainConfigOverriderTest.php rename to domain_config/tests/src/Functional/DomainConfigOverriderTest.php index 995219dd..160be3cb 100644 --- a/domain_config/src/Tests/DomainConfigOverriderTest.php +++ b/domain_config/tests/src/Functional/DomainConfigOverriderTest.php @@ -1,6 +1,6 @@ domainTableIsEmpty(); - // Create four new domains programmatically. + // Create five new domains programmatically. $this->domainCreateTestDomains(5); // Get the domain list. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); // Except for the default domain, the page title element should match what // is in the override files. // With a language context, based on how we have our files setup, we @@ -28,18 +28,10 @@ public function testDomainConfigOverrider() { // - example.com name = 'Drupal' for English, 'Drupal' for Spanish. // - one.example.com name = 'One' for English, 'Drupal' for Spanish. // - two.example.com name = 'Two' for English, 'Dos' for Spanish. - // - three.example.com name = 'Three' for English, 'Drupal' for Spanish. + // - three.example.com name = 'Drupal' for English, 'Drupal' for Spanish. // - four.example.com name = 'Four' for English, 'Four' for Spanish. foreach ($domains as $domain) { // Test the login page, because our default homepages do not exist. - $path = $domain->getPath() . 'user/login'; - $this->drupalGet($path); - if ($domain->isDefault()) { - $this->assertRaw('Log in | Drupal', 'Loaded the proper site name.'); - } - else { - $this->assertRaw('Log in | ' . $domain->label() . '', 'Loaded the proper site name.'); - } foreach ($this->langcodes as $langcode => $language) { $path = $domain->getPath() . $langcode . '/user/login'; $this->drupalGet($path); @@ -47,7 +39,7 @@ public function testDomainConfigOverrider() { $this->assertRaw('Log in | Drupal', 'Loaded the proper site name.'); } else { - $this->assertRaw('Log in | ' . $this->expectedName($domain) . '', 'Loaded the proper site name.'); + $this->assertRaw('Log in | ' . $this->expectedName($domain, $langcode) . '', 'Loaded the proper site name.' . 'Log in | ' . $this->expectedName($domain, $langcode) . ''); } } } @@ -69,9 +61,9 @@ public function testDomainConfigOverriderFromSettings() { ]; $this->writeSettings($settings); - // Create four new domains programmatically. + // Create five new domains programmatically. $this->domainCreateTestDomains(5); - $domains = \Drupal::service('domain.loader')->loadMultiple(['one_example_com', 'four_example_com']); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(['one_example_com', 'four_example_com']); $domain_one = $domains['one_example_com']; $this->drupalGet($domain_one->getPath() . 'user/login'); @@ -81,26 +73,32 @@ public function testDomainConfigOverriderFromSettings() { $this->drupalGet($domain_four->getPath() . 'user/login'); $this->assertRaw('Log in | Four overridden in settings', 'Found overridden slogan for four.example.com.'); } + /** * Returns the expected site name value from our test configuration. * - * @param DomainInterface $domain + * @param \Drupal\domain\DomainInterface $domain * The Domain object. + * @param string $langcode + * A two-digit language code. * * @return string * The expected name. */ - private function expectedName(DomainInterface $domain) { + private function expectedName(DomainInterface $domain, $langcode = NULL) { $name = ''; switch ($domain->id()) { case 'one_example_com': - case 'three_example_com': - $name = 'Drupal'; + $name = ($langcode == 'es') ? 'Drupal' : 'One'; break; case 'two_example_com': - $name = 'Dos'; + $name = ($langcode == 'es') ? 'Dos' : 'Two'; + break; + + case 'three_example_com': + $name = 'Drupal'; break; case 'four_example_com': diff --git a/domain_config/tests/src/Functional/DomainConfigPageCacheTest.php b/domain_config/tests/src/Functional/DomainConfigPageCacheTest.php new file mode 100644 index 00000000..5a55ebec --- /dev/null +++ b/domain_config/tests/src/Functional/DomainConfigPageCacheTest.php @@ -0,0 +1,50 @@ +domainTableIsEmpty(); + + // Create a new domain programmatically. + $this->domainCreateTestDomains(5); + $expected = []; + + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(NULL, TRUE); + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath()); + // The page cache includes a colon at the end. + $expected[] = $domain->getPath() . ':'; + } + + $database = \Drupal::database(); + $query = $database->query("SELECT cid FROM {cache_page}"); + $result = $query->fetchCol(); + + $this->assertEqual(sort($expected), sort($result), implode(', ', $result)); + + } + +} diff --git a/domain_config/src/Tests/DomainConfigTestBase.php b/domain_config/tests/src/Functional/DomainConfigTestBase.php similarity index 58% rename from domain_config/src/Tests/DomainConfigTestBase.php rename to domain_config/tests/src/Functional/DomainConfigTestBase.php index 0e900cfd..07e83d50 100644 --- a/domain_config/src/Tests/DomainConfigTestBase.php +++ b/domain_config/tests/src/Functional/DomainConfigTestBase.php @@ -1,9 +1,8 @@ 'Spanish'); + protected $langcodes = ['es' => 'Spanish']; /** * Modules to enable. * * @var array */ - public static $modules = array('domain', 'language', 'domain_config_test', 'domain_config'); + public static $modules = [ + 'domain', + 'language', + 'domain_config_test', + 'domain_config', + ]; /** * {@inheritdoc} @@ -41,18 +47,20 @@ protected function setUp() { parent::setUp(); // Create and login user. - $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages')); + $admin_user = $this->drupalCreateUser(['administer languages', 'access administration pages']); $this->drupalLogin($admin_user); // Add language. - $edit = array( + $edit = [ 'predefined_langcode' => 'es', - ); - $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + ]; + $this->drupalGet('admin/config/regional/language/add'); + $this->submitForm($edit, 'Add language'); // Enable URL language detection and selection. - $edit = array('language_interface[enabled][language-url]' => '1'); - $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + $edit = ['language_interface[enabled][language-url]' => '1']; + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm($edit, 'Save settings'); $this->drupalLogout(); @@ -61,7 +69,7 @@ protected function setUp() { $this->rebuildContainer(); $es = \Drupal::entityTypeManager()->getStorage('configurable_language')->load('es'); - $this->assertTrue(!empty($es), 'Created test language.'); + $this->assertNotEmpty($es, 'Created test language.'); } } diff --git a/domain_config_ui/README.md b/domain_config_ui/README.md index b94b52ed..b57248c5 100644 --- a/domain_config_ui/README.md +++ b/domain_config_ui/README.md @@ -1,9 +1,79 @@ Domain Config UI ================ -This module allows configuration to be saved for a selected domain. +This module allows configuration to be saved for a selected domain. It is intended to be used for simple settings (like the site name string). Complex settings like dates and languages may not be covered by this module. -Dependencies -============ +The module allows select editors to save settings on a per-domain and per-language basis. It removes some of the need to edit domain.config files manually. +## Permissions + +The module provides four permissions: + +* 'Administer Domain Config UI settings' + - Allows administrators to determine what forms are available for domain-specific configuration. Give only to administrators. +* 'Manage domain-specific configurations' + - Allows domain administrators to use configuration forms specific to their managed domains. +* 'Set the default configuration for all sites' + - Allows domain administrators to set the default value for configuration. The default value is used for all sites without a domain-specific configuration. +* 'Translate domain-specific configurations' + - Allows domain administrators to use language-specific configuration forms specific to their managed domains. + +Different form options will be provided to users based on these permissions. + +This behavior is covered by the *DomainConfigUIPermissionsTest* and the *DomainConfigUIOptionsTest*. + +## Form usage + +By default, the Appearance page and the Basic Site Settings page are enabled for domain-specific forms. Administrators may inspect and expand the form list at the settings page (admin/config/domain/config-ui). + +On admin forms that contain configuration, the administrator should see a buttom to 'Enable / Disable domain configuration'. This button can be used to add or remove a form from domain-sensitivity. + +*Note: removing a form will not remove its configuration files. See below.* + +When a form is domain-enabled, users with the *Manage domain-specific configurations* permission who are assigned to that domain and can access the form will be given the option to save the form for their domain. If the language module is enabled and the user has the *Translate domain-specific configurations* permission, then all language options will be shown as well. + +This behavior is covered by the *DomainConfigUiSettingsTest*. + +## Inspecting and deleting domain configuration + +Administrators can inspect stored domain configuration on the 'Saved configuration' page (admin/config/domain/config-ui/list). From here, you may inspect individual configuration files or delete those files. + +This behavior is covered by the *DomainConfigUiSavedConfigTest*. + +## An example + +In a case where we want to have a different Site slogan per domain, we can do the following: + +* Log in as an administrator with *Administer Domain Config UI settings*. +* Go to admin/config/system/site-information. +* Enable the form if it is not already enabled. +* Select a domain -- note that when you do, the page will reload and the settings values may change. +* Update the *Slogan* field. +* Save the form. +* Select another domain. +* Note that the slogan field is different than the one you saved. + +This behavior is covered by the *DomainConfigUIOverrideTest*. + +# Limitations + +As noted above, some administrative forms have complex handling that cannot be covered by this module. The color settings for the Bartik theme are a good example. Default language handling is another. Due to the extra processing required by these settings, we do not recommend using Domain Config UI for these settings. + +The proper function of any settings is *at the administrator's own risk*. Always test before deploying configuration to the live site. If a configuration override does not work, there may be numerous core reasons why. The most common is caching, addressed below. + +# Installation + +If some variable changes are not picked up when the page renders, you may need +add domain-sensitivity to the site's cache. + +To do so, clone `default.services.yml` to `services.yml` and change the +`required_cache_contexts` value to add the *url.site* context: + +```YAML + required_cache_contexts: ['languages:language_interface', 'theme', 'user.permissions', 'url.site'] +``` + +## Dependencies + +- Domain - Domain Config diff --git a/domain_config_ui/config/install/domain_config_ui.settings.yml b/domain_config_ui/config/install/domain_config_ui.settings.yml new file mode 100644 index 00000000..09068bcb --- /dev/null +++ b/domain_config_ui/config/install/domain_config_ui.settings.yml @@ -0,0 +1,2 @@ +remember_domain: false +path_pages: "/admin/appearance\r\n/admin/config/system/site-information" diff --git a/domain_config_ui/config/schema/domain_config_ui.schema.yml b/domain_config_ui/config/schema/domain_config_ui.schema.yml new file mode 100644 index 00000000..7b5639ff --- /dev/null +++ b/domain_config_ui/config/schema/domain_config_ui.schema.yml @@ -0,0 +1,11 @@ +# Schema for the configuration files of the Domain Config UI module. +domain_config_ui.settings: + type: config_object + label: 'Domain Config UI settings' + mapping: + remember_domain: + type: boolean + label: 'Remember domain selection' + path_pages: + type: text + label: 'Paths for supported configuration forms' diff --git a/domain_config_ui/domain_config_ui.info.yml b/domain_config_ui/domain_config_ui.info.yml index 2e4c9df0..e0f30dca 100644 --- a/domain_config_ui/domain_config_ui.info.yml +++ b/domain_config_ui/domain_config_ui.info.yml @@ -1,8 +1,13 @@ name: Domain Configuration UI -description: 'Allows saving of domain specific configuration through the UI.' +description: 'Allows saving of domain specific configuration through the administrative interface.' type: module package: Domain -version: VERSION -core: 8.x +core_version_requirement: ^8 || ^9 +configure: domain_config_ui.settings dependencies: - - domain_config + - domain:domain_config + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_config_ui/domain_config_ui.links.menu.yml b/domain_config_ui/domain_config_ui.links.menu.yml new file mode 100644 index 00000000..c7783b4c --- /dev/null +++ b/domain_config_ui/domain_config_ui.links.menu.yml @@ -0,0 +1,12 @@ +domain_config_ui.settings: + title: Domain config forms + route_name: domain_config_ui.settings + parent: domain.admin + description: 'Domain Config UI settings' + weight: 0 +domain_config_ui.list: + title: Saved configuration + route_name: domain_config_ui.list + parent: domain_config_ui.settings + description: 'Saved configuration' + weight: 0 diff --git a/domain_config_ui/domain_config_ui.links.task.yml b/domain_config_ui/domain_config_ui.links.task.yml new file mode 100644 index 00000000..4fc72e5d --- /dev/null +++ b/domain_config_ui/domain_config_ui.links.task.yml @@ -0,0 +1,14 @@ +domain_config_ui.settings: + title: Domain config forms + route_name: domain_config_ui.settings + base_route: domain.admin +domain_config_ui.settings_tab: + title: Settings + route_name: domain_config_ui.settings + parent_id: domain_config_ui.settings + weight: 10 +domain_config_ui.list: + title: Saved configuration + route_name: domain_config_ui.list + parent_id: domain_config_ui.settings + weight: 20 diff --git a/domain_config_ui/domain_config_ui.module b/domain_config_ui/domain_config_ui.module index 64b9eb44..567b0e3c 100644 --- a/domain_config_ui/domain_config_ui.module +++ b/domain_config_ui/domain_config_ui.module @@ -1,166 +1,181 @@ getForm('Drupal\domain_config_ui\Form\SwitchForm'); - $content = [ - 'domain_config_ui_switch' => $form, - ]; - - $variables['page']['content'] = array_merge($content, $variables['page']['content']); - - // Add a message below the form to remind the administrator which domain they are currently configuring. - if ($warning_message = domain_config_ui_save_warning_message()) { - $variables['page']['content']['domain_config_ui_switch_warning'] = $warning_message; - } -} - -/** - * Implements hook_form_alter(). - * - * @param array $form - * @param FormStateInterface $form_state - */ -function domain_config_ui_form_alter(&$form, FormStateInterface $form_state) { - // Only alter config forms that can have a config factory and are on an admin path. - if (!domain_config_ui_route_is_admin() || !domain_config_ui_form_is_allowed($form)) { - return; + if (!domain_config_ui_path_is_registered()) { + $content = ['domain_config_ui_admin' => domain_config_ui_admin_form('enable')]; } - - // Create fieldset to group domain fields. - $form['domain_config_ui'] = [ - '#type' => 'fieldset', - '#title' => 'Domain Configuration', - '#weight' => -10, - ]; - - // Add domain switch select field. - $selected_domain = \Drupal::service('domain_config_ui.manager')->getSelectedDomain(); - $form['domain_config_ui']['config_save_domain'] = [ - '#type' => 'select', - '#title' => 'Domain', - '#options' => array_merge(['' => 'All Domains'], \Drupal::service('domain.loader')->loadOptionsList()), - '#default_value' => $selected_domain ? $selected_domain->id() : '', - '#ajax' => [ - 'callback' => 'domain_config_ui_domain_switch_form_callback', - ], - ]; - - // Add language select field. - $selected_language = \Drupal::service('domain_config_ui.manager')->getSelectedLanguage(); - $language_options = ['' => 'Default']; - foreach (\Drupal::languageManager()->getLanguages() as $id => $language) { - $language_options[$id] = $language->getName(); + else { + $content = ['domain_config_ui_admin' => domain_config_ui_admin_form('disable')]; + // Add a message below the form showing the current domain. + $form = \Drupal::formBuilder()->getForm('Drupal\domain_config_ui\Form\SwitchForm'); + if (isset($form['domain_config_ui']['domain']['#options'])) { + $options = $form['domain_config_ui']['domain']['#options']; + } + if ($form['#access'] && $warning_message = domain_config_ui_save_warning_message($options)) { + $content['domain_config_ui_switch_warning'] = $warning_message; + } + // Add domain switch form to the top of the content region. + $content['domain_config_ui_switch'] = $form; } - $form['domain_config_ui']['config_save_language'] = [ - '#type' => 'select', - '#title' => 'Language', - '#options' => $language_options, - '#default_value' => $selected_language ? $selected_language->getId() : '', - '#ajax' => [ - 'callback' => 'domain_config_ui_domain_switch_form_callback', - ], - ]; - - // Add a message below the form to remind the administrator which domain they are currently configuring. - if ($warning_message = domain_config_ui_save_warning_message()) { - $form['domain_message'] = $warning_message; + if ($content) { + $variables['page']['content'] = array_merge($content, $variables['page']['content']); } } /** - * Helper to generate the markup for the domain save warning message. + * Generates the markup for the AJAX admin action. + * + * @param string $op + * An operation: either 'enable' or 'disable' are allowed. */ -function domain_config_ui_save_warning_message() { - $selected_domain = \Drupal::service('domain_config_ui.manager')->getSelectedDomain(); - if ($selected_domain) { - $selected_language = \Drupal::service('domain_config_ui.manager')->getSelectedLanguage(); - $message = new TranslatableMarkup('Configuration will be saved for @domain @language', [ - '@domain' => $selected_domain->label(), - '@language' => $selected_language ? '(' . $selected_language->getName() . ')' : '', - ]); - return [ - '#markup' => new FormattableMarkup('
@message
', [ - '@message' => $message, - ]), - '#weight' => 1000, +function domain_config_ui_admin_form($op) { + $admin_form = []; + if (\Drupal::currentUser()->hasPermission('administer domain config ui')) { + $route = \Drupal::routeMatch()->getRouteObject(); + // We make a special exception for the themes overview, which is unique. + // @TODO: make this list extensible. + $special_form = FALSE; + $special_paths = [ + '/admin/appearance', + '/admin/appearance/settings', + '/admin/appearance/settings/{theme}', ]; + if (in_array($route->getPath(), $special_paths, TRUE)) { + $special_form = TRUE; + } + if ($route->hasDefault('_form') || $special_form) { + $base_form = $route->getDefault('_form'); + if ($special_form || (is_callable($base_form, TRUE) && method_exists($base_form, 'getEditableConfigNames'))) { + $params = [ + 'op' => $op, + 'route_name' => \Drupal::routeMatch()->getRouteName(), + ]; + foreach (\Drupal::routeMatch()->getRawParameters() as $key => $value) { + $params[$key] = $value; + } + $title = new TranslatableMarkup('Enable domain configuration'); + if ($op == 'disable') { + $title = new TranslatableMarkup('Disable domain configuration'); + } + $admin_form = [ + '#type' => 'link', + '#url' => Url::fromRoute('domain_config_ui.inline_action', $params), + '#title' => $title, + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + 'button--small', + ], + ], + '#prefix' => '

', + '#suffix' => '

', + '#weight' => -10, + ]; + } + } } + return $admin_form; } /** - * Checks if provided form can be used to save domain specic configuration. + * Generates the markup for the domain save warning message. * - * @param array $form - * @return boolean + * @param array $domain_options + * The options for the domain element of the form. */ -function domain_config_ui_form_is_allowed($form) { - $allowed = [ - 'system_site_information_settings', - 'system_theme_settings', +function domain_config_ui_save_warning_message(array $domain_options = []) { + $manager = \Drupal::service('domain_config_ui.manager'); + if ($selected_domain_id = $manager->getSelectedDomainId()) { + $selected_domain = \Drupal::service('entity_type.manager') + ->getStorage('domain') + ->load($selected_domain_id); + } + if ($selected_language_id = $manager->getSelectedLanguageId()) { + $selected_language = \Drupal::service('language_manager') + ->getLanguage($selected_language_id); + } + $domain_label = !empty($selected_domain) ? + new TranslatableMarkup('the @label domain', ['@label' => $selected_domain->label()]) : + new TranslatableMarkup('all domains without custom configuration'); + + // In some cases, the user cannot use 'all domains.' In that case, we have to + // use the default option as a label. + if (empty($selected_domain) && !\Drupal::currentUser()->hasPermission('set default domain configuration')) { + $label = current($domain_options); + $domain_label = new TranslatableMarkup('the @label domain', ['@label' => $label]); + } + + $languages = \Drupal::service('language_manager')->getLanguages(); + if (count($languages) > 1) { + $language_label = !empty($selected_language) ? $selected_language->getName() : new TranslatableMarkup('all languages without custom configuration.'); + } + else { + $language_label = !empty($selected_language) ? $selected_language->getName() : new TranslatableMarkup('all languages.'); + } + $message = new TranslatableMarkup('This configuration will be saved for @domain and displayed in @language', [ + '@domain' => $domain_label, + '@language' => $language_label, + ]); + + return [ + '#markup' => new FormattableMarkup('
@message
', [ + '@message' => $message, + ]), + '#weight' => -1000, ]; - \Drupal::moduleHandler()->alter('domain_config_form_allowed', $allowed); - return in_array($form['#form_id'], $allowed); } /** - * Checks if provided path should have a domain switch form added to the top of the page. + * Checks if provided path should have a domain switch form on top of the page. * - * @return boolean + * @return bool + * TRUE if domain switch should be added. Otherwise, FALSE. */ -function domain_config_ui_route_is_allowed() { - $allowed = [ - '/admin/appearance', - ]; - \Drupal::moduleHandler()->alter('domain_config_route_allowed', $allowed); - $route = \Drupal::routeMatch()->getRouteObject(); - return in_array($route->getPath(), $allowed); +function domain_config_ui_path_is_registered() { + $path_pages = \Drupal::config('domain_config_ui.settings')->get('path_pages'); + // Theme settings pass arguments, so check both path and route. + $path = \Drupal::service('path.current')->getPath(); + + // Get internal path without language prefix. + $url = Url::fromUri('internal:' . $path); + $internal_path = '/' . $url->getInternalPath(); + + return \Drupal::service('path.matcher')->matchPath($internal_path, $path_pages); } /** * Checks if route is admin. * - * @return boolean + * @return bool + * TRUE if route is admin. Otherwise, FALSE. */ function domain_config_ui_route_is_admin() { $route = \Drupal::routeMatch()->getRouteObject(); + // Never allow this module's form to be added. + // @TODO: Allow modules to extend this list. + $disallowed = [ + '/admin/config/domain/config-ui', + '/admin/config/domain/settings', + ]; + if (in_array($route->getPath(), $disallowed, TRUE)) { + return FALSE; + } return \Drupal::service('router.admin_context')->isAdminRoute($route); } - -/** - * AJAX callback to set the current domain. - * - * @param array $form - * @param FormStateInterface $form_state - */ -function domain_config_ui_domain_switch_form_callback($form, FormStateInterface $form_state) { - // Switch the current domain. - \Drupal::service('domain_config_ui.manager')->setSelectedDomain($form_state->getValue('config_save_domain')); - - // Switch the current language. - \Drupal::service('domain_config_ui.manager')->setSelectedLanguage($form_state->getValue('config_save_language')); - - // Reset form with selected domain configuration. - $form_state->setUserInput([]); - $new_form = \Drupal::formBuilder()->rebuildForm($form['#form_id'], $form_state, $form); - $response = new AjaxResponse(); - $response->addCommand(new ReplaceCommand('.' . str_replace('_', '-', $form['#form_id']), $new_form)); - return $response; -} diff --git a/domain_config_ui/domain_config_ui.permissions.yml b/domain_config_ui/domain_config_ui.permissions.yml new file mode 100644 index 00000000..d81c2119 --- /dev/null +++ b/domain_config_ui/domain_config_ui.permissions.yml @@ -0,0 +1,16 @@ +administer domain config ui: + title: 'Administer Domain Config UI settings' + description: 'Allows administrators to determine what forms are available for domain-specific configuration.' + restrict access: true +set default domain configuration: + title: 'Set the default configuration for all sites' + description: 'Allows domain administrators to set the default value for a configuration. The default value is used for all sites without a domain-specific configuration.' + restrict access: true +translate domain configuration: + title: 'Translate domain-specific configurations' + description: 'Allows domain administrators to use language-specific configuration forms specific to their managed domains.' + restrict access: true +use domain config ui: + title: 'Manage domain-specific configurations' + description: 'Allows domain administrators to use configuration forms specific to their managed domains.' + restrict access: true diff --git a/domain_config_ui/domain_config_ui.routing.yml b/domain_config_ui/domain_config_ui.routing.yml new file mode 100644 index 00000000..2b4be106 --- /dev/null +++ b/domain_config_ui/domain_config_ui.routing.yml @@ -0,0 +1,38 @@ +domain_config_ui.settings: + path: '/admin/config/domain/config-ui' + defaults: + _title: 'Domain config forms' + _form: '\Drupal\domain_config_ui\Form\SettingsForm' + requirements: + _permission: 'administer domain config ui' +domain_config_ui.list: + path: '/admin/config/domain/config-ui/list' + defaults: + _title: 'Saved configuration' + _controller: '\Drupal\domain_config_ui\Controller\DomainConfigUIController::overview' + requirements: + _permission: 'administer domain config ui' +domain_config_ui.inline_action: + path: '/admin/config/domain/config_ui/{route_name}/{op}' + defaults: + _controller: '\Drupal\domain_config_ui\Controller\DomainConfigUIController::ajaxOperation' + requirements: + _permission: 'administer domain config ui' + _csrf_token: 'TRUE' + op: 'enable|disable' +domain_config_ui.inspect: + path: '/admin/config/domain/config_ui/inspect/{config_name}' + defaults: + _title: 'Inspect domain configuration' + _controller: '\Drupal\domain_config_ui\Controller\DomainConfigUIController::inspectConfig' + config_name: NULL + requirements: + _permission: 'administer domain config ui' +domain_config_ui.delete: + path: '/admin/config/domain/config_ui/delete/{config_name}' + defaults: + _title: 'Delete domain configuration' + _form: '\Drupal\domain_config_ui\Form\DeleteForm' + config_name: NULL + requirements: + _permission: 'administer domain config ui' diff --git a/domain_config_ui/domain_config_ui.services.yml b/domain_config_ui/domain_config_ui.services.yml index 361fac97..bfd7dd20 100644 --- a/domain_config_ui/domain_config_ui.services.yml +++ b/domain_config_ui/domain_config_ui.services.yml @@ -1,16 +1,12 @@ services: domain_config_ui.manager: class: Drupal\domain_config_ui\DomainConfigUIManager - arguments: ['@config.storage', '@domain.loader', '@language_manager'] - + arguments: ['@request_stack'] domain_config_ui.factory: class: Drupal\domain_config_ui\Config\ConfigFactory + decorates: config.factory + decoration_priority: 1 tags: - { name: event_subscriber } - { name: service_collector, tag: 'config.factory.override', call: addOverride } - arguments: ['@config.storage', '@event_dispatcher', '@config.typed'] - calls: - - [setDomainConfigUIManager, ['@domain_config_ui.manager']] - - config.factory: - alias: domain_config_ui.factory + arguments: ['@config.storage', '@event_dispatcher', '@config.typed', '@domain_config_ui.manager'] diff --git a/domain_config_ui/src/Config/Config.php b/domain_config_ui/src/Config/Config.php index b9990a0d..29016c63 100644 --- a/domain_config_ui/src/Config/Config.php +++ b/domain_config_ui/src/Config/Config.php @@ -9,18 +9,21 @@ * Extend core Config class to save domain specific configuration. */ class Config extends CoreConfig { + /** * The Domain config UI manager. * - * @var DomainConfigUIManager + * @var \Drupal\domain_config_ui\DomainConfigUIManager */ protected $domainConfigUIManager; /** * Set the Domain config UI manager. - * @param DomainConfigUIManager $domain_config_ui_manager + * + * @param \Drupal\domain_config_ui\DomainConfigUIManager $domain_config_ui_manager + * The Domain config UI manager. */ - public function setDomainConfigUIManager($domain_config_ui_manager) { + public function setDomainConfigUiManager(DomainConfigUIManager $domain_config_ui_manager) { $this->domainConfigUIManager = $domain_config_ui_manager; } @@ -35,8 +38,8 @@ public function save($has_trusted_data = FALSE) { // Get domain config name for saving. $domainConfigName = $this->getDomainConfigName(); - // If config is new and we are currently saving domain specific configuration, - // save with original name first so that there is always a default configuration. + // If config is new and we are saving domain specific configuration, + // save with original name so there is always a default configuration. if ($this->isNew && $domainConfigName != $originalName) { parent::save($has_trusted_data); } diff --git a/domain_config_ui/src/Config/ConfigFactory.php b/domain_config_ui/src/Config/ConfigFactory.php index 131b16c6..376a2e2c 100644 --- a/domain_config_ui/src/Config/ConfigFactory.php +++ b/domain_config_ui/src/Config/ConfigFactory.php @@ -1,66 +1,50 @@ allowedDomainConfig; - \Drupal::moduleHandler()->alter('domain_config_allowed', $allowed); - - // Return original name if reserved not allowed. - $is_allowed = FALSE; - foreach ($allowed as $config_name) { - // Convert config_name into into regex. - // Escapes regex syntax, but keeps * wildcards. - $pattern = '/^' . str_replace('\*', '.*', preg_quote($config_name, '/')) . '$/'; - if (preg_match($pattern, $name)) { - $is_allowed = TRUE; - } - } - - return $is_allowed; + public function __construct(StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config, DomainConfigUIManager $domain_config_ui_manager) { + parent::__construct($storage, $event_dispatcher, $typed_config); + $this->domainConfigUIManager = $domain_config_ui_manager; } /** - * {@inheritDoc} - * @see \Drupal\Core\Config\ConfigFactory::createConfigObject() + * {@inheritdoc} */ protected function createConfigObject($name, $immutable) { - if (!$immutable && $this->isAllowedDomainConfig($name)) { + if (!$immutable) { $config = new Config($name, $this->storage, $this->eventDispatcher, $this->typedConfigManager); // Pass the UI manager to the Config object. - $config->setDomainConfigUIManager($this->domainConfigUIManager); + $config->setDomainConfigUiManager($this->domainConfigUIManager); return $config; } return parent::createConfigObject($name, $immutable); @@ -69,22 +53,23 @@ protected function createConfigObject($name, $immutable) { /** * Set the Domain config UI manager. * - * @param DomainConfigUIManager $domain_config_ui_manager + * @param \Drupal\domain_config_ui\DomainConfigUIManager $domain_config_ui_manager + * The Domain config UI manager. */ - public function setDomainConfigUIManager($domain_config_ui_manager) { + public function setDomainConfigUiManager(DomainConfigUIManager $domain_config_ui_manager) { $this->domainConfigUIManager = $domain_config_ui_manager; } /** - * {@inheritDoc} - * @see \Drupal\Core\Config\ConfigFactory::doLoadMultiple() + * {@inheritdoc} */ protected function doLoadMultiple(array $names, $immutable = TRUE) { // Let parent load multiple load as usual. $list = parent::doLoadMultiple($names, $immutable); - // Do not apply overrides if configuring 'all' domains or config is immutable. - if (empty($this->domainConfigUIManager) || !$this->domainConfigUIManager->getSelectedDomainId() || !$this->isAllowedDomainConfig(current($names))) { + // Do not override if configuring 'all' domains or config is immutable. + // @TODO: This will need to change if we allow saving for 'all allowed domains' + if (empty($this->domainConfigUIManager) || !$this->domainConfigUIManager->getSelectedDomainId()) { return $list; } @@ -94,7 +79,7 @@ protected function doLoadMultiple(array $names, $immutable = TRUE) { $module_overrides = []; $storage_data = $this->storage->readMultiple($names); - // Load module overrides so that domain specific config is loaded in admin forms. + // Load module overrides so that domain config is loaded in admin forms. if (!empty($storage_data)) { // Only get domain overrides if we have configuration to override. $module_overrides = $this->loadDomainOverrides($names); @@ -103,12 +88,12 @@ protected function doLoadMultiple(array $names, $immutable = TRUE) { foreach ($storage_data as $name => $data) { $cache_key = $this->getConfigCacheKey($name, $immutable); - if (isset($module_overrides[$name])) { + if (!empty($module_overrides[$name])) { $this->cache[$cache_key]->setModuleOverride($module_overrides[$name]); $list[$name] = $this->cache[$cache_key]; + $this->propagateConfigOverrideCacheability($cache_key, $name); } - $this->propagateConfigOverrideCacheability($cache_key, $name); } } @@ -116,12 +101,11 @@ protected function doLoadMultiple(array $names, $immutable = TRUE) { } /** - * {@inheritDoc} - * @see \Drupal\Core\Config\ConfigFactory::doGet() + * {@inheritdoc} */ protected function doGet($name, $immutable = TRUE) { - // Do not apply overrides if configuring 'all' domains or config is immutable. - if (empty($this->domainConfigUIManager) || !$this->domainConfigUIManager->getSelectedDomainId() || !$this->isAllowedDomainConfig($name)) { + // If config for 'all' domains or immutable then don't override config. + if (empty($this->domainConfigUIManager) || !$this->domainConfigUIManager->getSelectedDomainId()) { return parent::doGet($name, $immutable); } @@ -133,7 +117,7 @@ protected function doGet($name, $immutable = TRUE) { // storage, create a new object. $config = $this->createConfigObject($name, $immutable); - // Load domain overrides so that domain specific config is loaded in admin forms. + // Load domain overrides so domain config is loaded in admin forms. $overrides = $this->loadDomainOverrides([$name]); if (isset($overrides[$name])) { $config->setModuleOverride($overrides[$name]); @@ -148,7 +132,7 @@ protected function doGet($name, $immutable = TRUE) { } /** - * Get arbitrary overrides for the named configuration objects from Domain module. + * Get Domain module overrides for the named configuration objects. * * @param array $names * The names of the configuration objects to get overrides for. @@ -157,6 +141,24 @@ protected function doGet($name, $immutable = TRUE) { * An array of overrides keyed by the configuration object name. */ protected function loadDomainOverrides(array $names) { - return $this->domainConfigUIManager->loadOverrides($names); + $overrides = []; + foreach ($names as $name) { + // Try to load the language-specific domain override. + $config_name = $this->domainConfigUIManager->getSelectedConfigName($name); + if ($override = $this->storage->read($config_name)) { + $overrides[$name] = $override; + } + // If we tried to load a language-sensitive file and failed, load the + // domain-specific override. + elseif ($this->domainConfigUIManager->getSelectedLanguageId()) { + $omit_language = TRUE; + $config_name = $this->domainConfigUIManager->getSelectedConfigName($name, $omit_language); + if ($override = $this->storage->read($config_name)) { + $overrides[$name] = $override; + } + } + } + return $overrides; } + } diff --git a/domain_config_ui/src/Controller/DomainConfigUIController.php b/domain_config_ui/src/Controller/DomainConfigUIController.php new file mode 100644 index 00000000..35602688 --- /dev/null +++ b/domain_config_ui/src/Controller/DomainConfigUIController.php @@ -0,0 +1,281 @@ +getCurrentRequest()->getQueryString(); + $params = []; + $parts = explode('&', $query); + foreach ($parts as $part) { + $element = explode('=', $part); + if ($element[0] !== 'token') { + $params[$element[0]] = $element[1]; + } + } + $url = Url::fromRoute($route_name, $params); + + // Get current module settings. + $config = \Drupal::configFactory()->getEditable('domain_config_ui.settings'); + $path_pages = $this->standardizePaths($config->get('path_pages')); + $new_path = '/' . $url->getInternalPath(); + + if (!$url->isExternal() && $url->access()) { + switch ($op) { + case 'enable': + // Check to see if we already registered this form. + if (!$exists = \Drupal::service('path.matcher')->matchPath($new_path, $path_pages)) { + $this->addPath($new_path); + $message = $this->t('Form added to domain configuration interface.'); + $success = TRUE; + } + break; + + case 'disable': + if ($exists = \Drupal::service('path.matcher')->matchPath($new_path, $path_pages)) { + $this->removePath($new_path); + $message = $this->t('Form removed from domain configuration interface.'); + $success = TRUE; + } + break; + } + } + // Set a message. + if ($success) { + \Drupal::messenger()->addMessage($message); + } + else { + \Drupal::messenger()->addError($this->t('The operation failed.')); + } + // Return to the invoking page. + return new RedirectResponse($url->toString(), 302); + } + + /** + * Lists all stored configuration. + */ + public function overview() { + $elements = []; + $page['table'] = [ + '#type' => 'table', + '#header' => [ + 'name' => t('Configuration key'), + 'item' => t('Item'), + 'domain' => t('Domain'), + 'language' => t('Language'), + 'actions' => t('Actions'), + ], + ]; + // @TODO: inject services. + $storage = \Drupal::service('config.storage'); + foreach ($storage->listAll('domain.config') as $name) { + $elements[] = $this->deriveElements($name); + } + // Sort the items. + if (!empty($elements)) { + uasort($elements, [$this, 'sortItems']); + foreach ($elements as $element) { + $operations = [ + 'inspect' => [ + 'url' => Url::fromRoute('domain_config_ui.inspect', ['config_name' => $element['name']]), + 'title' => $this->t('Inspect'), + ], + 'delete' => [ + 'url' => Url::fromRoute('domain_config_ui.delete', ['config_name' => $element['name']]), + 'title' => $this->t('Delete'), + ], + ]; + $page['table'][] = [ + 'name' => ['#markup' => $element['name']], + 'item' => ['#markup' => $element['item']], + 'domain' => ['#markup' => $element['domain']], + 'language' => ['#markup' => $element['language']], + 'actions' => ['#type' => 'operations', '#links' => $operations], + ]; + } + } + else { + $page = [ + '#markup' => $this->t('No domain-specific configurations have been found.'), + ]; + } + return $page; + } + + /** + * Controller for inspecting configuration. + * + * @param string $config_name + * The domain config object being inspected. + */ + public function inspectConfig($config_name = NULL) { + if (empty($config_name)) { + $url = Url::fromRoute('domain_config_ui.list'); + return new RedirectResponse($url->toString()); + } + $elements = $this->deriveElements($config_name); + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + if ($elements['language'] == $this->t('all')->render()) { + $language = $this->t('all languages'); + } + else { + $language = $this->t('the @language language.', ['@language' => $elements['language']]); + } + $page['help'] = [ + '#type' => 'item', + '#title' => Html::escape($config_name), + '#markup' => $this->t('This configuration is for the %domain domain and + applies to %language.', [ + '%domain' => $elements['domain'], + '%language' => $language, + ] + ), + '#prefix' => '

', + '#suffix' => '

', + ]; + $page['text'] = [ + '#markup' => $this->printArray($config), + ]; + return $page; + } + + /** + * Derives the parts of a config object for presentation. + * + * @param string $name + * A configuration object name. + * + * @return array + * An array of config values, keyed by name. + */ + public static function deriveElements($name) { + $entity_manager = \Drupal::entityTypeManager(); + $items = explode('.', $name); + $elements = [ + 'prefix' => $items[0], + 'config' => isset($items[1]) && isset($items[2]) ? $items[1] : '', + 'domain' => isset($items[2]) && isset($items[3]) ? $items[2] : '', + 'language' => isset($items[3]) && isset($items[4]) && strlen($items[3]) == 2 ? $items[3] : '', + ]; + + $elements['item'] = trim(str_replace($elements, '', $name), '.'); + + if (!empty($elements['domain']) && $domain = $entity_manager->getStorage('domain')->load($elements['domain'])) { + $elements['domain'] = $domain->label(); + } + + if (!$elements['language']) { + // Static context requires use of t() here. + $elements['language'] = t('all')->render(); + } + elseif ($language = \Drupal::languageManager()->getLanguage($elements['language'])) { + $elements['language'] = $language->getName(); + } + + $elements['name'] = $name; + + return $elements; + } + + /** + * Sorts items by parent config. + */ + public function sortItems($a, $b) { + return strcmp($a['item'], $b['item']); + } + + /** + * Prints array data for the form. + * + * @param array $array + * An array of data. Note that we support two levels of nesting. + * + * @return string + * A suitable output string. + */ + public static function printArray(array $array) { + $items = []; + foreach ($array as $key => $val) { + if (!is_array($val)) { + $value = self::formatValue($val); + $item = [ + '#theme' => 'item_list', + '#items' => [$value], + '#title' => self::formatValue($key), + ]; + $items[] = render($item); + } + else { + $list = []; + foreach ($val as $k => $v) { + $list[] = t('@key : @value', ['@key' => $k, '@value' => self::formatValue($v)]); + } + $variables = [ + '#theme' => 'item_list', + '#items' => $list, + '#title' => self::formatValue($key), + ]; + $items[] = render($variables); + } + } + $rendered = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + return render($rendered); + } + + /** + * Formats a value as a string, for readable output. + * + * Taken from config_inspector module. + * + * @param mixed $value + * The value element. + * + * @return string + * The value in string form. + */ + protected static function formatValue($value) { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_scalar($value)) { + return Html::escape($value); + } + if (empty($value)) { + return '<' . t('empty') . '>'; + } + return '<' . gettype($value) . '>'; + } + +} diff --git a/domain_config_ui/src/DomainConfigUIManager.php b/domain_config_ui/src/DomainConfigUIManager.php index b031a681..a7c83542 100644 --- a/domain_config_ui/src/DomainConfigUIManager.php +++ b/domain_config_ui/src/DomainConfigUIManager.php @@ -2,175 +2,88 @@ namespace Drupal\domain_config_ui; -use Drupal\domain\DomainLoaderInterface; -use Drupal\domain\DomainInterface; -use Drupal\Core\Config\StorageInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\Language\LanguageInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * Domain Config UI manager. */ -class DomainConfigUIManager { - /** - * A storage controller instance for reading and writing configuration data. - * - * @var StorageInterface - */ - protected $storage; - - /** - * Domain loader. - * - * @var DomainLoaderInterface - */ - protected $domainLoader; +class DomainConfigUIManager implements DomainConfigUIManagerInterface { /** - * Language manager. + * A RequestStack instance. * - * @var LanguageManagerInterface + * @var \Symfony\Component\HttpFoundation\RequestStack */ - protected $languageManager; + protected $requestStack; /** - * The domain context of the request. + * The current request. * - * @var DomainInterface $domain + * @var \Symfony\Component\HttpFoundation\Request */ - protected $domain; + protected $currentRequest; /** - * The language context of the request. + * Constructs DomainConfigUIManager object. * - * @var LanguageInterface $language - */ - protected $language; - - /** - * Constructs domain config UI service. - * - * @param StorageInterface $storage - * The configuration storage engine. - * @param DomainLoaderInterface $domain_loader - * The domain loader. - */ - public function __construct(StorageInterface $storage, DomainLoaderInterface $domain_loader, LanguageManagerInterface $language_manager) { - $this->storage = $storage; - $this->domainLoader = $domain_loader; - $this->languageManager = $language_manager; - - // Get the language context. - if ($language = $this->getSelectedLanguage()) { - $this->language = $language; - } - - // Get the domain context. - if ($domain = $this->getSelectedDomain()) { - $this->domain = $domain; - } - } - - /** - * Load only overrides for selected domain and language. + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * The request stack. */ - public function loadOverrides($names) { - $overrides = []; - if (!empty($this->domain)) { - foreach ($names as $name) { - $config_name = $this->getSelectedConfigName($name); - if ($override = $this->storage->read($config_name)) { - $overrides[$name] = $override; - } - } - } - return $overrides; + public function __construct(RequestStack $request_stack) { + // We want the currentRequest, but it is not always available. + // https://www.drupal.org/project/domain/issues/3004243#comment-13700917 + $this->requestStack = $request_stack; } /** - * Get selected config name. - * @param string $name + * {@inheritdoc} */ - public function getSelectedConfigName($name) { - // Build prefix and add to front of existing key. - if ($selected_domain = $this->getSelectedDomain()) { - $prefix = 'domain.config.' . $selected_domain->id() . '.'; - // Add selected language. - if ($language = $this->getSelectedLanguage()) { - $prefix .= $language->getId() . '.'; + public function getSelectedConfigName($name, $omit_language = FALSE) { + if ($domain_id = $this->getSelectedDomainId()) { + $prefix = "domain.config.{$domain_id}."; + if (!$omit_language && $langcode = $this->getSelectedLanguageId()) { + $prefix .= "{$langcode}."; } - $name = $prefix . $name; + return $prefix . $name; } return $name; } /** - * Get the selected domain. - */ - public function getSelectedDomain() { - $selected_domain_id = $this->getSelectedDomainId(); - if ($selected_domain_id && $selected_domain = $this->domainLoader->load($selected_domain_id)) { - return $selected_domain; - } - } - - /** - * Get the selected domain ID. + * {@inheritdoc} */ public function getSelectedDomainId() { - return !empty($_SESSION['domain_config_ui']['config_save_domain']) ? $_SESSION['domain_config_ui']['config_save_domain'] : ''; - } - - /** - * Set the current selected domain ID. - * @param string $domain_id - */ - public function setSelectedDomain($domain_id) { - if ($domain = $this->domainLoader->load($domain_id)) { - // Set session for subsequent request. - $_SESSION['domain_config_ui']['config_save_domain'] = $domain_id; - // Switch active domain now so that selected domain configuration can be loaded immediatly. - // This is primarily for switching domain with AJAX request. - $this->domain = $domain; + if (!empty($this->getRequest()) && $domain = $this->currentRequest->get('domain_config_ui_domain')) { + return $domain; } - else { - $_SESSION['domain_config_ui']['config_save_domain'] = ''; - unset($this->domain); + elseif (isset($_SESSION['domain_config_ui_domain'])) { + return $_SESSION['domain_config_ui_domain']; } } /** - * Set the selected language. - * @param string $language_id + * {@inheritdoc} */ - public function setSelectedLanguage($language_id) { - if ($language = $this->languageManager->getLanguage($language_id)) { - // Set session for subsequent request. - $_SESSION['domain_config_ui']['config_save_language'] = $language_id; - // Switch active language now so that selected domain configuration can be loaded immediatly. - // This is primarily for switching domain with AJAX request. - $this->language = $language; + public function getSelectedLanguageId() { + if (!empty($this->getRequest()) && $language = $this->currentRequest->get('domain_config_ui_language')) { + return $language; } - else { - $_SESSION['domain_config_ui']['config_save_language'] = ''; - unset($this->language); + elseif (isset($_SESSION['domain_config_ui_language'])) { + return $_SESSION['domain_config_ui_language']; } } /** - * Get the selected language ID. - */ - public function getSelectedLanguageId() { - return !empty($_SESSION['domain_config_ui']['config_save_language']) ? $_SESSION['domain_config_ui']['config_save_language'] : ''; - } - - /** - * Get the selected language. + * Ensures that the currentRequest is loaded. + * + * @return Symfony\Component\HttpFoundation\Request|null + * The current request object. */ - public function getSelectedLanguage() { - $selected_language_id = $this->getSelectedLanguageId(); - if ($selected_language_id && $selected_language = $this->languageManager->getLanguage($selected_language_id)) { - return $selected_language; + private function getRequest() { + if (!isset($this->currentRequest)) { + $this->currentRequest = $this->requestStack->getCurrentRequest(); } + return $this->currentRequest; } + } diff --git a/domain_config_ui/src/DomainConfigUIManagerInterface.php b/domain_config_ui/src/DomainConfigUIManagerInterface.php new file mode 100644 index 00000000..acbad075 --- /dev/null +++ b/domain_config_ui/src/DomainConfigUIManagerInterface.php @@ -0,0 +1,39 @@ +getEditable('domain_config_ui.settings'); + $path_string = $config->get('path_pages'); + + $path_array = $this->explodePathSettings($path_string); + $path_array[] = $new_path; + + $path_string = $this->implodePathSettings($path_array); + $config->set('path_pages', $path_string)->save(); + + return $path_string; + } + + /** + * Removes a path from the registry. + * + * @param string $old_path + * The path to remove. + * + * @return string + * The normalized path that was removed. + */ + public function removePath($old_path) { + $config = \Drupal::configFactory()->getEditable('domain_config_ui.settings'); + $path_string = $config->get('path_pages'); + + $path_array = $this->explodePathSettings($path_string); + $list = array_flip($path_array); + if (isset($list[$old_path])) { + unset($list[$old_path]); + } + $path_array = array_flip($list); + + $path_string = $this->implodePathSettings($path_array); + $config->set('path_pages', $path_string)->save(); + + return $path_string; + } + + /** + * Turns an array of paths into a linebreak separated string. + * + * @param array $path_array + * An array of registered paths. + * + * @return string + * A normalized string of paths. + */ + public function implodePathSettings(array $path_array) { + return implode("\r\n", $path_array); + } + + /** + * Turns the path string into an array. + * + * @param string $path_string + * An string of registered paths. + * + * @return array + * A normalized array of paths. + */ + public function explodePathSettings($path_string) { + // Replace newlines with a logical 'or'. + $find = '/(\\r\\n?|\\n)/'; + $replace = '|'; + $list = preg_replace($find, $replace, $path_string); + return explode("|", $list); + } + + /** + * Normalizes the path string using \r\n for linebreaks. + * + * @param string $path_string + * The string of paths. + * + * @return string + * A normalized path string. + */ + public function standardizePaths($path_string) { + return $this->implodePathSettings($this->explodePathSettings($path_string)); + } + +} diff --git a/domain_config_ui/src/Form/DeleteForm.php b/domain_config_ui/src/Form/DeleteForm.php new file mode 100644 index 00000000..2006e7c8 --- /dev/null +++ b/domain_config_ui/src/Form/DeleteForm.php @@ -0,0 +1,100 @@ +toString()); + } + + $elements = DomainConfigUIController::deriveElements($config_name); + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $form['help'] = [ + '#type' => 'item', + '#title' => Html::escape($config_name), + '#markup' => $this->t('Are you sure you want to delete the configuration + override: %config_name?', ['%config_name' => $config_name]), + '#prefix' => '

', + '#suffix' => '

', + ]; + if ($elements['language'] == $this->t('all')->render()) { + $language = $this->t('all languages'); + } + else { + $language = $this->t('the @language language.', ['@language' => $elements['language']]); + } + $form['more_help'] = [ + '#markup' => $this->t('This configuration is for the %domain domain and + applies to %language.', [ + '%domain' => $elements['domain'], + '%language' => $language, + ] + ), + '#prefix' => '

', + '#suffix' => '

', + ]; + $form['review'] = [ + '#type' => 'details', + '#title' => $this->t('Review settings'), + '#open' => FALSE, + ]; + $form['review']['text'] = [ + '#markup' => DomainConfigUIController::printArray($config), + ]; + $form['config_name'] = ['#type' => 'value', '#value' => $config_name]; + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete configuration'), + '#button_type' => 'primary', + ]; + $form['actions']['cancel'] = [ + '#type' => 'link', + '#title' => $this->t('Cancel'), + '#url' => new Url('domain_config_ui.list'), + '#attributes' => [ + 'class' => [ + 'button', + ], + ], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $name = $form_state->getValue('config_name'); + $message = $this->t('Domain configuration %label has been deleted.', ['%label' => $name]); + \Drupal::messenger()->addMessage($message); + \Drupal::logger('domain_config')->notice($message); + \Drupal::configFactory()->getEditable($name)->delete(); + $form_state->setRedirectUrl(new Url('domain_config_ui.list')); + } + +} diff --git a/domain_config_ui/src/Form/SettingsForm.php b/domain_config_ui/src/Form/SettingsForm.php new file mode 100644 index 00000000..a75f5514 --- /dev/null +++ b/domain_config_ui/src/Form/SettingsForm.php @@ -0,0 +1,90 @@ +config('domain_config_ui.settings'); + $form['remember_domain'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Remember domain selection'), + '#default_value' => $config->get('remember_domain'), + '#description' => $this->t('Keeps last selected domain when loading new configuration forms.'), + ]; + $form['pages'] = [ + '#title' => $this->t('Enabled configuration forms'), + '#type' => 'details', + '#open' => TRUE, + ]; + $form['pages']['path_pages'] = [ + '#type' => 'textarea', + '#rows' => 5, + '#columns' => 40, + '#default_value' => $this->standardizePaths($config->get('path_pages')), + '#description' => $this->t("Specify pages by using their paths. Enter one path per line. Paths must start with /admin. Wildcards (*) are not supported. An example path is /admin/appearance for the Appearance page."), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $path_string = $form_state->getValue('path_pages'); + $path_array = $this->explodePathSettings($path_string); + $exists = []; + foreach ($path_array as $path) { + if (in_array($path, $exists, TRUE)) { + $form_state->setError($form['pages']['path_pages'], $this->t('Duplicate paths cannot be added')); + } + $exists[] = $path; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Clean session values. + unset($_SESSION['domain_config_ui_domain']); + unset($_SESSION['domain_config_ui_language']); + + $path_string = $form_state->getValue('path_pages'); + $path_array = $this->explodePathSettings($path_string); + + $this->config('domain_config_ui.settings') + ->set('remember_domain', $form_state->getValue('remember_domain')) + ->set('path_pages', $this->implodePathSettings($path_array)) + ->save(); + parent::submitForm($form, $form_state); + } + +} diff --git a/domain_config_ui/src/Form/SwitchForm.php b/domain_config_ui/src/Form/SwitchForm.php index 04f8b0fb..aaf36353 100644 --- a/domain_config_ui/src/Form/SwitchForm.php +++ b/domain_config_ui/src/Form/SwitchForm.php @@ -1,36 +1,107 @@ domainConfigUiManager = $domain_config_ui_manager; + $this->languageManager = $language_manager; + $this->entityTypeManager = $entity_type_manager; + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); + $this->domainElementManager = $domain_element_manager; + // Not loaded directly since it is not an interface. + $this->accessHandler = $this->entityTypeManager->getAccessControlHandler('domain'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('language_manager'), + $container->get('domain_config_ui.manager'), + $container->get('domain.element_manager') + ); + } + + /** + * {@inheritdoc} */ public function getFormId() { return 'domain_config_ui_switch_form'; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { // Only allow access to domain administrators. - $form['#access'] = $this->currentUser()->hasPermission('administer domains'); + $form['#access'] = $this->canUseDomainConfig(); $form = $this->addSwitchFields($form, $form_state); return $form; } + /** + * Determines if a user may access the domain-sensitive form. + */ + public function canUseDomainConfig() { + if ($this->currentUser()->hasPermission('administer domains')) { + $user_domains = 'all'; + } + else { + $account = $this->currentUser(); + $user = $this->entityTypeManager->getStorage('user')->load($account->id()); + $user_domains = $this->domainElementManager->getFieldValues($user, DomainInterface::DOMAIN_ADMIN_FIELD); + } + $permission = $this->currentUser()->hasPermission('use domain config ui') || + $this->currentUser()->hasPermission('administer domain config ui'); + return (!empty($user_domains) && $permission); + } + /** * Helper to add switch fields to form. * * @param array $form - * @param FormStateInterface $form_state + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state array. */ public function addSwitchFields(array $form, FormStateInterface $form_state) { // Create fieldset to group domain fields. @@ -41,38 +112,54 @@ public function addSwitchFields(array $form, FormStateInterface $form_state) { ]; // Add domain switch select field. - $selected_domain = \Drupal::service('domain_config_ui.manager')->getSelectedDomain(); - $form['domain_config_ui']['config_save_domain'] = [ - '#type' => 'select', - '#title' => 'Domain', - '#options' => array_merge(['' => 'All Domains'], \Drupal::service('domain.loader')->loadOptionsList()), - '#default_value' => $selected_domain ? $selected_domain->id() : '', - '#ajax' => [ - 'callback' => '::switchCallback', - ], - ]; - - // Add language select field. - $selected_language = \Drupal::service('domain_config_ui.manager')->getSelectedLanguage(); - $language_options = ['' => 'Default']; - foreach (\Drupal::languageManager()->getLanguages() as $id => $language) { - $language_options[$id] = $language->getName(); + if ($selected_domain_id = $this->domainConfigUiManager->getSelectedDomainId()) { + $selected_domain = $this->domainStorage->load($selected_domain_id); } - $form['domain_config_ui']['config_save_language'] = [ + // Get the form options. + $form['domain_config_ui']['domain'] = [ '#type' => 'select', - '#title' => 'Language', - '#options' => $language_options, - '#default_value' => $selected_language ? $selected_language->getId() : '', + '#title' => $this->t('Domain'), + '#options' => $this->getDomainOptions(), + '#default_value' => !empty($selected_domain) ? $selected_domain->id() : '', '#ajax' => [ 'callback' => '::switchCallback', ], ]; + // Add language select field. Domain Config does not rely on core's Config + // Translation module, so we set our own permission. + $languages = $this->languageManager->getLanguages(); + if (count($languages) > 1 && $this->currentUser()->hasPermission('translate domain configuration')) { + $language_options = ['' => $this->t('Default')]; + foreach ($languages as $id => $language) { + if (!$language->isLocked()) { + $language_options[$id] = $language->getName(); + } + } + $form['domain_config_ui']['language'] = [ + '#type' => 'select', + '#title' => $this->t('Language'), + '#options' => $language_options, + '#default_value' => $this->domainConfigUiManager->getSelectedLanguageId(), + '#ajax' => [ + 'callback' => '::switchCallback', + ], + ]; + $form['domain_config_ui']['help'] = [ + '#markup' => $this->t('Changing the domain or language will load its active configuration.'), + ]; + } + else { + $form['domain_config_ui']['help'] = [ + '#markup' => $this->t('Changing the domain will load its active configuration.'), + ]; + } + // @TODO: Add cache contexts to form? return $form; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { // Form does not require submit handler. @@ -82,18 +169,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * Callback to remember save mode and reload page. * * @param array $form - * @param FormStateInterface $form_state + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state array. */ public static function switchCallback(array &$form, FormStateInterface $form_state) { - // Switch the current domain. - \Drupal::service('domain_config_ui.manager')->setSelectedDomain($form_state->getValue('config_save_domain')); - - // Switch the current language. - \Drupal::service('domain_config_ui.manager')->setSelectedLanguage($form_state->getValue('config_save_language')); - // Extract requesting page URI from ajax URI. // Copied from Drupal\Core\Form\FormBuilder::buildFormAction(). - $request_uri = \Drupal::service('request_stack')->getMasterRequest()->getRequestUri(); + $request = \Drupal::service('request_stack')->getMasterRequest(); + $request_uri = $request->getRequestUri(); // Prevent cross site requests via the Form API by using an absolute URL // when the request uri starts with multiple slashes. @@ -103,6 +187,18 @@ public static function switchCallback(array &$form, FormStateInterface $form_sta $parsed = UrlHelper::parse($request_uri); unset($parsed['query']['ajax_form'], $parsed['query'][MainContentViewSubscriber::WRAPPER_FORMAT]); + + if (\Drupal::config('domain_config_ui.settings')->get('remember_domain')) { + // Save domain and language on session. + $_SESSION['domain_config_ui_domain'] = $form_state->getValue('domain'); + $_SESSION['domain_config_ui_language'] = $form_state->getValue('language'); + } + else { + // Pass domain and language as request query parameters. + $parsed['query']['domain_config_ui_domain'] = $form_state->getValue('domain'); + $parsed['query']['domain_config_ui_language'] = $form_state->getValue('language'); + } + $request_uri = $parsed['path'] . ($parsed['query'] ? ('?' . UrlHelper::buildQuery($parsed['query'])) : ''); // Reload the page to get new form values. @@ -110,4 +206,30 @@ public static function switchCallback(array &$form, FormStateInterface $form_sta $response->addCommand(new RedirectCommand($request_uri)); return $response; } + + /** + * Gets the available domain list for the form user. + * + * @return array + * An array of select options. + */ + public function getDomainOptions() { + $domains = $this->domainStorage->loadMultipleSorted(); + $options = []; + foreach ($domains as $domain) { + // If the user cannot view the domain, then don't show in the list. + // View here is sufficient, because it means the user is assigned to the + // domain. We have already checked for the ability to use this form. + $access = $this->accessHandler->checkAccess($domain, 'view'); + if ($access->isAllowed()) { + $options[$domain->id()] = $domain->label(); + } + } + // The user must have permission to set the default value. + if ($this->currentUser()->hasPermission('set default domain configuration')) { + $options = array_merge(['' => $this->t('All Domains')], $options); + } + return $options; + } + } diff --git a/domain_config_ui/tests/src/Functional/DomainConfigUIOptionsTest.php b/domain_config_ui/tests/src/Functional/DomainConfigUIOptionsTest.php new file mode 100644 index 00000000..505c13af --- /dev/null +++ b/domain_config_ui/tests/src/Functional/DomainConfigUIOptionsTest.php @@ -0,0 +1,140 @@ +createAdminUser(); + $this->createLimitedUser(); + $this->createLanguageUser(); + + $this->domainCreateTestDomains(5); + // Assign the adminUser and editorUser to some domains. + $this->addDomainsToEntity('user', $this->limitedUser->id(), ['example_com', 'one_example_com'], DomainInterface::DOMAIN_ADMIN_FIELD); + $this->addDomainsToEntity('user', $this->languageUser->id(), ['two_example_com', 'three_example_com'], DomainInterface::DOMAIN_ADMIN_FIELD); + } + + /** + * Tests access the the settings form. + */ + public function testFormOptions() { + $this->drupalLogin($this->adminUser); + $path = '/admin/config/domain/config-ui'; + $path2 = '/admin/config/system/site-information'; + + // Visit the domain config ui administration page. + $this->drupalGet($path); + $this->assertResponse(200); + + // Visit the site information page. + $this->drupalGet($path2); + $this->assertResponse(200); + $this->findField('domain'); + $this->findField('language'); + + // We expect to find five domain options. + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + foreach ($domains as $domain) { + $string = 'value="' . $domain->id() . '"'; + $this->assertRaw($string, 'Found the domain option.'); + } + // We expect to find 'All Domains'. + $this->assertRaw('All Domains', 'Found the domain option.'); + + // We expect to find two language options. + $languages = ['en', 'es']; + foreach ($languages as $langcode) { + $string = 'value="' . $langcode . '"'; + $this->assertRaw($string, 'Found the language option.'); + } + + // Now test the editorUser. + $this->drupalLogin($this->limitedUser); + + // Visit the domain config ui administration page. + $this->drupalGet($path); + $this->assertResponse(403); + + // Visit the site information page. + $this->drupalGet($path2); + $this->assertResponse(200); + $this->findField('domain'); + $this->findNoField('language'); + + // We expect to find two domain options. + foreach ($domains as $domain) { + $string = 'value="' . $domain->id() . '"'; + if (in_array($domain->id(), ['example_com', 'one_example_com'], TRUE)) { + $this->assertRaw($string, 'Found the domain option.'); + } + else { + $this->assertNoRaw($string, 'Did not find the domain option.'); + } + } + + // We expect to find 'All Domains'. + $this->assertRaw('All Domains', 'Found the domain option.'); + + // Now test the languageUser. + $this->drupalLogin($this->languageUser); + + // Visit the domain config ui administration page. + $this->drupalGet($path); + $this->assertResponse(403); + + // Visit the site information page. + $this->drupalGet($path2); + $this->assertResponse(200); + $this->findField('domain'); + $this->findField('language'); + + // We expect to find two domain options. + foreach ($domains as $domain) { + $string = 'value="' . $domain->id() . '"'; + if (in_array($domain->id(), ['two_example_com', 'three_example_com'], TRUE)) { + $this->assertRaw($string, 'Found the domain option.'); + } + else { + $this->assertNoRaw($string, 'Did not find the domain option.'); + } + } + + // We do not expect to find 'All Domains'. + $this->assertNoRaw('All Domains', 'Found the domain option.'); + + // We expect to find two language options. + $languages = ['en', 'es']; + foreach ($languages as $langcode) { + $string = 'value="' . $langcode . '"'; + $this->assertRaw($string, 'Found the language option.'); + } + + } + +} diff --git a/domain_config_ui/tests/src/Functional/DomainConfigUIPermissionsTest.php b/domain_config_ui/tests/src/Functional/DomainConfigUIPermissionsTest.php new file mode 100644 index 00000000..95812596 --- /dev/null +++ b/domain_config_ui/tests/src/Functional/DomainConfigUIPermissionsTest.php @@ -0,0 +1,67 @@ +createAdminUser(); + $this->createEditorUser(); + + $this->domainCreateTestDomains(5); + } + + /** + * Tests access the the settings form. + */ + public function testSettingsAccess() { + $this->drupalLogin($this->adminUser); + $path = '/admin/config/domain/config-ui'; + $path2 = '/admin/config/system/site-information'; + + // Visit the domain config ui administration page. + $this->drupalGet($path); + $this->assertResponse(200); + + // Visit the site information page. + $this->drupalGet($path2); + $this->assertResponse(200); + $this->findField('domain'); + + $this->drupalLogin($this->editorUser); + + // Visit the domain config ui administration page. + $this->drupalGet($path); + $this->assertResponse(403); + + // Visit the site information page. + $this->drupalGet($path2); + $this->assertResponse(200); + $this->findNoField('domain'); + } + +} diff --git a/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUIOverrideTest.php b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUIOverrideTest.php new file mode 100644 index 00000000..aea20140 --- /dev/null +++ b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUIOverrideTest.php @@ -0,0 +1,146 @@ +createAdminUser(); + $this->createEditorUser(); + + $this->setBaseHostname(); + $this->domainCreateTestDomains(5); + + $this->createLanguage(); + } + + /** + * Tests that we can save domain and language-specific settings. + */ + public function testAjax() { + // Test base configuration. + $config_name = 'system.site'; + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $this->assertEquals($config['name'], 'Drupal'); + $this->assertEquals($config['page']['front'], '/user/login'); + + // Test stored configuration. + $config_name = 'domain.config.one_example_com.en.system.site'; + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $this->assertEquals($config['name'], 'One'); + $this->assertEquals($config['page']['front'], '/node/1'); + + $this->drupalLogin($this->adminUser); + $path = '/admin/config/system/site-information'; + + // Visit the site information page. + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + + // Test our form. + $page->findField('domain'); + $page->findField('language'); + $page->selectFieldOption('domain', 'one_example_com'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->fillField('site_name', 'New name'); + $page->fillField('site_frontpage', '/user'); + $this->htmlOutput($page->getHtml()); + $page->pressButton('Save configuration'); + $this->htmlOutput($page->getHtml()); + + // We did not save a language prefix, so none will be present. + $config_name = 'domain.config.one_example_com.system.site'; + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $this->assertEquals($config['name'], 'New name'); + $this->assertEquals($config['page']['front'], '/user'); + + // Now let's save a language. + // Visit the site information page. + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + + // Test our form. + $page->selectFieldOption('domain', 'one_example_com'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->selectFieldOption('language', 'es'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->fillField('site_name', 'Neuvo nombre'); + $page->fillField('site_frontpage', '/user'); + $this->htmlOutput($page->getHtml()); + $page->pressButton('Save configuration'); + $this->htmlOutput($page->getHtml()); + + // We did save a language prefix, so one will be present. + $config_name = 'domain.config.one_example_com.es.system.site'; + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $this->assertEquals($config['name'], 'Neuvo nombre'); + $this->assertEquals($config['page']['front'], '/user'); + + // Make sure the base is untouched. + $config_name = 'system.site'; + $config = \Drupal::configFactory()->get($config_name)->getRawData(); + + $this->assertEquals($config['name'], 'Drupal'); + $this->assertEquals($config['page']['front'], '/user/login'); + } + +} diff --git a/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUISettingsTest.php b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUISettingsTest.php new file mode 100644 index 00000000..91801784 --- /dev/null +++ b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUISettingsTest.php @@ -0,0 +1,132 @@ +createAdminUser(); + $this->createEditorUser(); + + $this->setBaseHostname(); + $this->domainCreateTestDomains(5); + + $this->createLanguage(); + } + + /** + * Tests ability to add/remove forms. + */ + public function testSettings() { + $config = $this->config('domain_config_ui.settings'); + $expected = $this->explodePathSettings("/admin/appearance\r\n/admin/config/system/site-information"); + $value = $this->explodePathSettings($config->get('path_pages')); + $this->assertEquals($expected, $value); + + // Test with language and without. + foreach (['en', 'es'] as $langcode) { + $config->save(); + $prefix = ''; + if ($langcode == 'es') { + $prefix = '/es'; + } + $this->drupalLogin($this->adminUser); + // Test some theme paths. + $path = $prefix . '/admin/appearance'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $page->findLink('Disable domain configuration'); + + $path = $prefix . '/admin/appearance/settings/stark'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $page->findLink('Enable domain configuration'); + $page->clickLink('Enable domain configuration'); + + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->drupalGet($path); + $config2 = $this->config('domain_config_ui.settings'); + $expected2 = $this->explodePathSettings("/admin/appearance\r\n/admin/config/system/site-information\r\n/admin/appearance/settings/stark"); + $value2 = $this->explodePathSettings($config2->get('path_pages')); + $this->assertEquals($expected2, $value2); + + // Test removal of paths. + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $page->findLink('Disable domain configuration'); + $page->clickLink('Disable domain configuration'); + + $this->assertSession()->assertWaitOnAjaxRequest(); + + $path = $prefix . '/admin/config/system/site-information'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $page->findLink('Disable domain configuration'); + $page->clickLink('Disable domain configuration'); + + $this->assertSession()->assertWaitOnAjaxRequest(); + + $expected3 = $this->explodePathSettings("/admin/appearance"); + $config3 = $this->config('domain_config_ui.settings'); + $value3 = $this->explodePathSettings($config3->get('path_pages')); + $this->assertEquals($expected3, $value3); + + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $page->findLink('Enable domain configuration'); + + // Ensure the editor cannot access the form. + $this->drupalLogin($this->editorUser); + $this->drupalGet($path); + $this->assertSession()->pageTextNotContains('Enable domain configuration'); + } + } + +} diff --git a/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUiSavedConfigTest.php b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUiSavedConfigTest.php new file mode 100644 index 00000000..483133ff --- /dev/null +++ b/domain_config_ui/tests/src/FunctionalJavascript/DomainConfigUiSavedConfigTest.php @@ -0,0 +1,182 @@ +createAdminUser(); + $this->createEditorUser(); + + $this->setBaseHostname(); + $this->domainCreateTestDomains(5); + + $this->createLanguage(); + } + + /** + * Tests that we can save domain and language-specific settings. + */ + public function testSavedConfig() { + $this->drupalLogin($this->adminUser); + $path = '/admin/config/system/site-information'; + + // Visit the site information page. + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + + // Test our form. + $page->findField('domain'); + $page->findField('language'); + $page->selectFieldOption('domain', 'one_example_com'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->fillField('site_name', 'New name'); + $page->fillField('site_frontpage', '/user'); + $this->htmlOutput($page->getHtml()); + $page->pressButton('Save configuration'); + $this->htmlOutput($page->getHtml()); + + // Now let's save a language. + // Visit the site information page. + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + + // Test our form. + $page->selectFieldOption('domain', 'one_example_com'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->selectFieldOption('language', 'es'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->htmlOutput($page->getHtml()); + + $page = $this->getSession()->getPage(); + $page->fillField('site_name', 'Neuvo nombre'); + $page->fillField('site_frontpage', '/user'); + $this->htmlOutput($page->getHtml()); + $page->pressButton('Save configuration'); + $this->htmlOutput($page->getHtml()); + + // Now, head to /admin/config/domain/config-ui/list. + $path = '/admin/config/domain/config-ui/list'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $this->htmlOutput($page->getHtml()); + $this->assertSession()->pageTextContains('Saved configuration'); + $this->assertSession()->pageTextContains('domain.config.one_example_com.system.site'); + $this->assertSession()->pageTextContains('domain.config.one_example_com.es.system.site'); + $this->assertSession()->pageTextNotContains('domain.config.example_com.en.system.site'); + + $page->findLink('Inspect'); + $page->clickLink('Inspect'); + $page = $this->getSession()->getPage(); + $this->htmlOutput($page->getHtml()); + $this->assertSession()->pageTextContains('domain.config.one_example_com.es.system.site'); + $this->assertSession()->pageTextContains('Neuvo nombre'); + + $path = '/admin/config/domain/config_ui/inspect/domain.config.one_example_com.system.site'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $this->htmlOutput($page->getHtml()); + $this->assertSession()->pageTextContains('domain.config.one_example_com.system.site'); + $this->assertSession()->pageTextContains('New name'); + + $path = '/admin/config/domain/config_ui/delete/domain.config.one_example_com.system.site'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $this->htmlOutput($page->getHtml()); + $this->assertSession()->pageTextContains('Are you sure you want to delete the configuration override: domain.config.one_example_com.system.site?'); + $page->findButton('Delete configuration'); + $page->pressButton('Delete configuration'); + + // Now, head to /admin/config/domain/config-ui/list. + $path = '/admin/config/domain/config-ui/list'; + $this->drupalGet($path); + $page = $this->getSession()->getPage(); + $this->htmlOutput($page->getHtml()); + $this->assertSession()->pageTextContains('Saved configuration'); + $this->assertSession()->pageTextNotContains('domain.config.one_example_com.system.site'); + $this->assertSession()->pageTextContains('domain.config.one_example_com.es.system.site'); + $this->assertSession()->pageTextNotContains('domain.config.example_com.en.system.site'); + + } + + /** + * Creates a second language for testing overrides. + */ + private function createLanguage() { + // Create and login user. + $adminUser = $this->drupalCreateUser(['administer languages', 'access administration pages']); + $this->drupalLogin($adminUser); + + // Add language. + $edit = [ + 'predefined_langcode' => 'es', + ]; + $this->drupalGet('admin/config/regional/language/add'); + $this->submitForm($edit, 'Add language'); + + // Enable URL language detection and selection. + $edit = ['language_interface[enabled][language-url]' => '1']; + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm($edit, 'Save settings'); + + $this->drupalLogout(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + $es = \Drupal::entityTypeManager()->getStorage('configurable_language')->load('es'); + $this->assertTrue(!empty($es), 'Created test language.'); + } + +} diff --git a/domain_config_ui/tests/src/Traits/DomainConfigUITestTrait.php b/domain_config_ui/tests/src/Traits/DomainConfigUITestTrait.php new file mode 100644 index 00000000..d9fc16a7 --- /dev/null +++ b/domain_config_ui/tests/src/Traits/DomainConfigUITestTrait.php @@ -0,0 +1,122 @@ +adminUser = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'administer domains', + 'administer domain config ui', + 'administer site configuration', + 'administer languages', + 'administer themes', + 'set default domain configuration', + 'translate domain configuration', + 'use domain config ui', + 'view domain information', + ]); + } + + /** + * Create an editor user. + */ + public function createEditorUser() { + $this->editorUser = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'administer site configuration', + 'administer languages', + ]); + } + + /** + * Create a limited admin user. + */ + public function createLimitedUser() { + $this->limitedUser = $this->drupalCreateUser([ + 'access administration pages', + 'administer languages', + 'administer site configuration', + 'use domain config ui', + 'set default domain configuration', + ]); + } + + /** + * Create a language administrator. + */ + public function createLanguageUser() { + $this->languageUser = $this->drupalCreateUser([ + 'access administration pages', + 'use domain config ui', + 'translate domain configuration', + 'administer site configuration', + ]); + } + + /** + * Creates a second language for testing overrides. + */ + public function createLanguage() { + // Create and login user. + $adminUser = $this->drupalCreateUser(['administer languages', 'access administration pages']); + $this->drupalLogin($adminUser); + + // Add language. + $edit = [ + 'predefined_langcode' => 'es', + ]; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + // Enable URL language detection and selection. + $edit = ['language_interface[enabled][language-url]' => '1']; + $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings')); + + $this->drupalLogout(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + $es = \Drupal::entityTypeManager()->getStorage('configurable_language')->load('es'); + $this->assertTrue(!empty($es), 'Created test language.'); + } + +} diff --git a/domain_content/config/optional/views.view.domain_content.yml b/domain_content/config/optional/views.view.affiliated_content.yml similarity index 98% rename from domain_content/config/optional/views.view.domain_content.yml rename to domain_content/config/optional/views.view.affiliated_content.yml index d3081f90..5452d21e 100644 --- a/domain_content/config/optional/views.view.domain_content.yml +++ b/domain_content/config/optional/views.view.affiliated_content.yml @@ -8,8 +8,6 @@ dependencies: - domain_access - node - user -_core: - default_config_hash: 80am62bndx8zJNcM94qCHJcsfULTTuUIZHd13f6UNyQ id: affiliated_content label: 'Affiliated content' module: node @@ -22,16 +20,15 @@ display: default: display_options: access: - type: perm - options: - perm: 'access domain content' + type: domain_content_editor + options: { } cache: type: tag query: type: views_query options: disable_sql_rewrite: true - distinct: true + distinct: false replica: false query_comment: '' query_tags: { } @@ -329,17 +326,17 @@ display: click_sort_column: target_id type: entity_reference_label settings: - link: 1 + link: true group_column: target_id group_columns: { } - group_rows: 1 - delta_limit: '0' - delta_offset: '0' - delta_reversed: 0 - delta_first_last: 0 + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false multi_type: separator separator: ', ' - field_api_classes: 0 + field_api_classes: false plugin_id: domain_access_field field_domain_all_affiliates: id: field_domain_all_affiliates @@ -528,7 +525,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: true expose: @@ -773,7 +770,7 @@ display: operation: view multiple: 0 access: false - bundles: null + bundles: { } glossary: false limit: 0 case: none @@ -805,8 +802,8 @@ display: - 'languages:language_interface' - url - url.query_args + - user - 'user.node_grants:view' - - user.permissions max-age: 0 tags: - 'config:field.storage.node.field_domain_access' @@ -840,7 +837,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: true expose: @@ -1019,8 +1016,8 @@ display: - 'languages:language_interface' - url - url.query_args + - user - 'user.node_grants:view' - - user.permissions max-age: 0 tags: - 'config:field.storage.node.field_domain_access' @@ -1048,7 +1045,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: true expose: @@ -1216,10 +1213,7 @@ display: group_type: group admin_label: '' operator: '=' - value: - min: '' - max: '' - value: '1' + value: '1' group: 1 exposed: false expose: @@ -1246,7 +1240,7 @@ display: default_group: All default_group_multiple: { } group_items: { } - plugin_id: numeric + plugin_id: boolean field_domain_access_target_id: id: field_domain_access_target_id table: node__field_domain_access @@ -1272,7 +1266,7 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1297,8 +1291,8 @@ display: - 'languages:language_interface' - url - url.query_args + - user - 'user.node_grants:view' - - user.permissions tags: - 'config:field.storage.node.field_domain_access' - 'config:field.storage.node.field_domain_all_affiliates' diff --git a/domain_content/config/optional/views.view.domain_content_editors.yml b/domain_content/config/optional/views.view.affiliated_editors.yml similarity index 98% rename from domain_content/config/optional/views.view.domain_content_editors.yml rename to domain_content/config/optional/views.view.affiliated_editors.yml index 50db88cc..fd65a6d2 100644 --- a/domain_content/config/optional/views.view.domain_content_editors.yml +++ b/domain_content/config/optional/views.view.affiliated_editors.yml @@ -7,8 +7,6 @@ dependencies: module: - domain_access - user -_core: - default_config_hash: Xfk6mJlu1ulvrGc8liItGZNh4BpK_lzdlC_85g3HSgE id: affiliated_editors label: 'Affiliated editors' module: user @@ -25,16 +23,15 @@ display: position: 0 display_options: access: - type: perm - options: - perm: 'access domain content editors' + type: domain_content_admin + options: { } cache: type: tag query: type: views_query options: disable_sql_rewrite: false - distinct: true + distinct: false replica: false query_comment: '' query_tags: { } @@ -405,17 +402,17 @@ display: click_sort_column: target_id type: entity_reference_label settings: - link: 0 + link: false group_column: target_id group_columns: { } - group_rows: 1 - delta_limit: '0' - delta_offset: '0' - delta_reversed: 0 - delta_first_last: 0 + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false multi_type: separator separator: ', ' - field_api_classes: 0 + field_api_classes: false plugin_id: domain_access_field field_domain_all_affiliates: id: field_domain_all_affiliates @@ -839,7 +836,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: true expose: @@ -887,7 +884,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: false expose: @@ -1014,7 +1011,7 @@ display: - 'languages:language_interface' - url - url.query_args - - user.permissions + - user max-age: 0 tags: - 'config:field.storage.user.field_domain_access' @@ -1083,7 +1080,7 @@ display: operation: view multiple: 0 access: false - bundles: null + bundles: { } glossary: false limit: 0 case: none @@ -1097,7 +1094,7 @@ display: - 'languages:language_interface' - url - url.query_args - - user.permissions + - user max-age: 0 tags: - 'config:field.storage.user.field_domain_access' @@ -1241,7 +1238,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: true expose: @@ -1289,7 +1286,7 @@ display: group_type: group admin_label: '' operator: '=' - value: true + value: '1' group: 1 exposed: false expose: @@ -1384,7 +1381,7 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' - reduce: 0 + reduce: false is_grouped: false group_info: label: '' @@ -1451,7 +1448,7 @@ display: - 'languages:language_interface' - url - url.query_args - - user.permissions + - user tags: - 'config:field.storage.user.field_domain_access' - 'config:field.storage.user.field_domain_all_affiliates' diff --git a/domain_content/config/schema/domain_content.views.schema.yml b/domain_content/config/schema/domain_content.views.schema.yml new file mode 100644 index 00000000..16f22354 --- /dev/null +++ b/domain_content/config/schema/domain_content.views.schema.yml @@ -0,0 +1,8 @@ +# Schema for the domain content plugins. + +views.access.domain_content_admin: + type: mapping + label: 'Domain Content: View domain-specific editors' +views.access.domain_content_editor: + type: mapping + label: 'Domain Content: View domain-specific content' diff --git a/domain_content/domain_content.info.yml b/domain_content/domain_content.info.yml index b8876efe..dfb82724 100644 --- a/domain_content/domain_content.info.yml +++ b/domain_content/domain_content.info.yml @@ -2,11 +2,16 @@ name: Domain Content description: 'Review content by assigned domain.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - domain - - domain_access - - node - - user - - views + - drupal:node + - drupal:user + - drupal:views + - domain:domain + - domain:domain_access + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_content/domain_content.install b/domain_content/domain_content.install new file mode 100644 index 00000000..dda7b361 --- /dev/null +++ b/domain_content/domain_content.install @@ -0,0 +1,60 @@ +getStorage('node_type')->loadMultiple(); + foreach ($node_types as $type => $info) { + $list[$type] = 'node'; + } + // Check for required fields. + foreach ($list as $bundle => $entity_type) { + $id = $entity_type . '.' . $bundle . '.' . DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD; + if (!$field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { + $allow = FALSE; + break; + } + $id = $entity_type . '.' . $bundle . '.' . DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD; + if (!$field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { + $allow = FALSE; + break; + } + } + } + if (!$allow) { + $requirements['domain_content'] = [ + 'title' => t('Domain content'), + 'description' => t('Domain content cannot be enabled until Domain access has installed its required fields.'), + 'severity' => REQUIREMENT_ERROR, + ]; + } + return $requirements; +} + +/** + * Implements hook_uninstall(). + */ +function domain_content_uninstall() { + $storage = \Drupal::entityTypeManager()->getStorage('view'); + $entities = []; + foreach (['affiliated_content', 'affiliated_editors'] as $id) { + if ($view = $storage->load($id)) { + $entities[$id] = $view; + } + } + if (!empty($entities)) { + $storage->delete($entities); + } +} diff --git a/domain_content/domain_content.module b/domain_content/domain_content.module index 06132d3d..836b788b 100644 --- a/domain_content/domain_content.module +++ b/domain_content/domain_content.module @@ -1,5 +1,10 @@ getStorage('user')->load($account->id()); $allowed = \Drupal::service('domain_access.manager')->getAccessValues($user); $id = $domain->id(); if ($account->hasPermission('publish to any domain') || ($account->hasPermission('publish to any assigned domain') && isset($allowed[$domain->id()]))) { - $operations['domain_content'] = array( + $operations['domain_content'] = [ 'title' => t('Content'), 'url' => Url::fromUri("internal:/admin/content/domain-content/$id"), // Core operations start at 0 and increment by 10. 'weight' => 120, - ); + ]; } if ($account->hasPermission('assign editors to any domain') || ($account->hasPermission('assign domain editors') && isset($allowed[$domain->id()]))) { - $operations['domain_users'] = array( + $operations['domain_users'] = [ 'title' => t('Editors'), 'url' => Url::fromUri("internal:/admin/content/domain-editors/$id"), // Core operations start at 0 and increment by 10. 'weight' => 120, - ); + ]; } return $operations; diff --git a/domain_content/src/Controller/DomainContentController.php b/domain_content/src/Controller/DomainContentController.php index aa1f88d1..e5c81970 100644 --- a/domain_content/src/Controller/DomainContentController.php +++ b/domain_content/src/Controller/DomainContentController.php @@ -3,10 +3,10 @@ namespace Drupal\domain_content\Controller; use Drupal\Core\Link; -use Drupal\domain\DomainInterface; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; +use Drupal\domain\DomainInterface; +use Drupal\domain_access\DomainAccessManagerInterface; /** * Controller routines domain content pages. @@ -14,29 +14,37 @@ class DomainContentController extends ControllerBase { /** - * Generates a list of content by domain. + * Builds the list of domains and relevant entities. + * + * @param array $options + * A list of variables required to build editor or content pages. + * + * @see contentlist() + * + * @return array + * A Drupal page build array. */ - public function contentList() { + public function buildList(array $options) { $account = $this->getUser(); - $permission = 'publish to any assigned domain'; $build = [ '#theme' => 'table', - '#header' => [$this->t('Domain'), $this->t('Content count')], + '#header' => [$this->t('Domain'), $options['column_header']], ]; - if ($account->hasPermission('publish to any domain')) { + if ($account->hasPermission($options['all_permission'])) { $build['#rows'][] = [ - Link::fromTextAndUrl($this->t('All affiliates'), Url::fromUri('internal:/admin/content/domain-content/all_affiliates')), - $this->getCount('node'), + Link::fromTextAndUrl($this->t('All affiliates'), Url::fromUri('internal:/admin/content/' . $options['path'] . '/all_affiliates')), + $this->getCount($options['type']), ]; } // Loop through domains. - $domains = \Drupal::service('domain.loader')->loadMultipleSorted(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultipleSorted(); + $manager = \Drupal::service('domain_access.manager'); /** @var \Drupal\domain\DomainInterface $domain */ foreach ($domains as $domain) { - if ($account->hasPermission('publish to any domain') || $this->allowAccess($account, $domain, $permission)) { + if ($account->hasPermission($options['all_permission']) || $manager->hasDomainPermissions($account, $domain, [$options['permission']])) { $row = [ - Link::fromTextAndUrl($domain->label(), Url::fromUri('internal:/admin/content/domain-content/' . $domain->id())), - $this->getCount('node', $domain), + Link::fromTextAndUrl($domain->label(), Url::fromUri('internal:/admin/content/' . $options['path'] . '/' . $domain->id())), + $this->getCount($options['type'], $domain), ]; $build['#rows'][] = $row; } @@ -44,35 +52,34 @@ public function contentList() { return $build; } + /** + * Generates a list of content by domain. + */ + public function contentList() { + $options = [ + 'type' => 'node', + 'column_header' => $this->t('Content count'), + 'permission' => 'publish to any assigned domain', + 'all_permission' => 'publish to any domain', + 'path' => 'domain-content', + ]; + + return $this->buildList($options); + } + /** * Generates a list of editors by domain. */ public function editorsList() { - $account = $this->getUser(); - $permission = 'assign domain editors'; - $build = [ - '#theme' => 'table', - '#header' => [$this->t('Domain'), $this->t('Editor count')], + $options = [ + 'type' => 'user', + 'column_header' => $this->t('Editor count'), + 'permission' => 'assign domain editors', + 'all_permission' => 'assign editors to any domain', + 'path' => 'domain-editors', ]; - if ($account->hasPermission('assign editors to any domain')) { - $build['#rows'][] = [ - Link::fromTextAndUrl($this->t('All affiliates'), Url::fromUri('internal:/admin/content/domain-editors/all_affiliates')), - $this->getCount('user'), - ]; - } - // Loop through domains. - $domains = \Drupal::service('domain.loader')->loadMultipleSorted(); - /** @var \Drupal\domain\DomainInterface $domain */ - foreach ($domains as $domain) { - if ($account->hasPermission('assign editors to any domain') || $this->allowAccess($account, $domain, $permission)) { - $row = [ - Link::fromTextAndUrl($domain->label(), Url::fromUri('internal:/admin/content/domain-editors/' . $domain->id())), - $this->getCount('user', $domain), - ]; - $build['#rows'][] = $row; - } - } - return $build; + + return $this->buildList($options); } /** @@ -80,7 +87,7 @@ public function editorsList() { * * @param string $entity_type * The entity type. - * @param DomainInterface $domain + * @param \Drupal\domain\DomainInterface $domain * The domain to query. If passed NULL, checks status for all affiliates. * * @return int @@ -88,16 +95,17 @@ public function editorsList() { */ protected function getCount($entity_type = 'node', DomainInterface $domain = NULL) { if (is_null($domain)) { - $field = DOMAIN_ACCESS_ALL_FIELD; + $field = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD; $value = 1; } else { - $field = DOMAIN_ACCESS_FIELD; + $field = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD; $value = $domain->id(); } // Note that we ignore node access so these queries work on any domain. $query = \Drupal::entityQuery($entity_type) - ->condition($field, $value); + ->condition($field, $value) + ->accessCheck(FALSE); return count($query->execute()); } @@ -105,7 +113,7 @@ protected function getCount($entity_type = 'node', DomainInterface $domain = NUL /** * Returns a fully loaded user object for the current request. * - * @return AccountInterface + * @return \Drupal\Core\Session\AccountInterface * The current user object. */ protected function getUser() { @@ -114,25 +122,4 @@ protected function getUser() { return \Drupal::entityTypeManager()->getStorage('user')->load($account->id()); } - /** - * Checks that a user can access the internal page for a domain list. - * - * @param AccountInterface $account - * The fully loaded user account. - * @param DomainInterface $domain - * The domain being checked. - * @param string $permission - * The relevant permission to check. - * - * @return bool - * Returns TRUE if the user can access the domain list page. - */ - protected function allowAccess(AccountInterface $account, DomainInterface $domain, $permission) { - $allowed = \Drupal::service('domain_access.manager')->getAccessValues($account); - if ($account->hasPermission($permission) && isset($allowed[$domain->id()])) { - return TRUE; - } - return FALSE; - } - } diff --git a/domain_content/src/Plugin/views/access/DomainContentAccess.php b/domain_content/src/Plugin/views/access/DomainContentAccess.php new file mode 100644 index 00000000..10b1ff40 --- /dev/null +++ b/domain_content/src/Plugin/views/access/DomainContentAccess.php @@ -0,0 +1,30 @@ +setRequirement('_permission', 'access domain content'); + } + +} diff --git a/domain_content/src/Plugin/views/access/DomainEditorAccess.php b/domain_content/src/Plugin/views/access/DomainEditorAccess.php new file mode 100644 index 00000000..eed22abb --- /dev/null +++ b/domain_content/src/Plugin/views/access/DomainEditorAccess.php @@ -0,0 +1,30 @@ +setRequirement('_permission', 'access domain content editors'); + } + +} diff --git a/domain_content/tests/src/Functional/DomainContentActionsTest.php b/domain_content/tests/src/Functional/DomainContentActionsTest.php new file mode 100644 index 00000000..0dd95377 --- /dev/null +++ b/domain_content/tests/src/Functional/DomainContentActionsTest.php @@ -0,0 +1,83 @@ +admin_user = $this->drupalCreateUser([ + 'administer domains', + 'access administration pages', + 'access domain content', + 'access domain content editors', + 'publish to any domain', + 'assign editors to any domain', + // Edit access is required. This is fastest. + 'bypass node access', + ]); + $this->drupalLogin($this->admin_user); + + // Create users and content. + $this->createDomainContent(); + + $url = 'admin/content/domain-content/all_affiliates'; + + $this->drupalGet($url); + + // All the content should be on domain one. + $old_domain = $this->domains['one_example_com']; + $new_domain = $this->domains['two_example_com']; + + // Domains are linked in the output. + $this->assertRaw($old_domain->label() . ''); + $this->assertNoRaw($new_domain->label() . ''); + + // Add some content to domain two. + $edit = [ + 'node_bulk_form[0]' => TRUE, + 'node_bulk_form[1]' => TRUE, + 'action' => 'domain_access_add_action.two_example_com', + ]; + $this->submitForm($edit, 'Apply to selected items'); + + // Both domains should be present. + $this->assertRaw($old_domain->label() . ''); + $this->assertRaw($new_domain->label() . ''); + + // Remove some content from domain two. + $edit = [ + 'node_bulk_form[0]' => TRUE, + 'node_bulk_form[1]' => TRUE, + 'action' => 'domain_access_remove_action.two_example_com', + ]; + $this->submitForm($edit, 'Apply to selected items'); + + // Domains are linked properly in the output. + $this->assertRaw($old_domain->label() . ''); + $this->assertNoRaw($new_domain->label() . ''); + + // There should be five elements. + $this->assertRaw('node_bulk_form[4]'); + + // Remove one from all affiliates. + $edit = [ + 'node_bulk_form[0]' => TRUE, + 'action' => 'domain_access_none_action', + ]; + $this->submitForm($edit, 'Apply to selected items'); + + // There should be four elements. + $this->assertRaw('node_bulk_form[3]'); + $this->assertNoRaw('node_bulk_form[4]'); + } + +} diff --git a/domain_content/tests/src/Functional/DomainContentCountTest.php b/domain_content/tests/src/Functional/DomainContentCountTest.php new file mode 100644 index 00000000..4c23e98f --- /dev/null +++ b/domain_content/tests/src/Functional/DomainContentCountTest.php @@ -0,0 +1,52 @@ +admin_user = $this->drupalCreateUser([ + 'administer domains', + 'access administration pages', + 'access domain content', + 'access domain content editors', + 'publish to any domain', + 'assign editors to any domain', + ]); + $this->drupalLogin($this->admin_user); + + // Create users and content. + $this->createDomainContent(); + $this->createDomainUsers(); + + // Base Urls for our views. + $urls = [ + 'admin/content/domain-content', + 'admin/content/domain-editors', + ]; + // Test the overview pages. + foreach ($urls as $url) { + $content = $this->drupalGet($url); + $this->assertResponse(200); + // Find the links. + $this->findLink('All affiliates'); + foreach ($this->domains as $id => $domain) { + $this->findLink($domain->label()); + $string = $domain->label() . "5"; + $this->checkContent($content, $string); + } + $string = 'All affiliates5'; + $this->checkContent($content, $string); + } + } + +} diff --git a/domain_content/tests/src/Functional/DomainContentPermissionsTest.php b/domain_content/tests/src/Functional/DomainContentPermissionsTest.php new file mode 100644 index 00000000..cd4c2ef7 --- /dev/null +++ b/domain_content/tests/src/Functional/DomainContentPermissionsTest.php @@ -0,0 +1,178 @@ +admin_user = $this->drupalCreateUser([ + 'administer domains', + 'access administration pages', + 'access domain content', + 'access domain content editors', + 'publish to any domain', + 'assign editors to any domain', + ]); + $this->drupalLogin($this->admin_user); + + // Create users and content. + $this->createDomainContent(); + $this->createDomainUsers(); + + // Base Urls for our views. + $urls = [ + 'admin/content/domain-content', + 'admin/content/domain-editors', + ]; + // Test the overview and domain-specific pages. + foreach ($urls as $url) { + $this->drupalGet($url); + $this->assertResponse(200); + // Find the links. + $this->findLink('All affiliates'); + foreach ($this->domains as $id => $domain) { + $this->findLink($domain->label()); + } + + // All affiliates link. + $this->drupalGet($url . '/all_affiliates'); + $this->assertResponse(200); + + // Individual domain pages. + foreach ($this->domains as $id => $domain) { + $this->drupalGet($url . '/' . $id); + $this->assertResponse(200); + } + } + // This user should be able to see everything but all affiliates. + $this->limited_user = $this->drupalCreateUser([ + 'administer domains', + 'access administration pages', + 'access domain content', + 'access domain content editors', + 'publish to any assigned domain', + 'assign domain editors', + ]); + $this->addDomainsToEntity('user', $this->limited_user->id(), array_keys($this->domains), DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + $this->drupalLogin($this->limited_user); + // Test the overview and domain-specific pages. + foreach ($urls as $url) { + $this->drupalGet($url); + $this->assertResponse(200); + // Find the links. + $this->assertNoRaw('All affiliates'); + foreach ($this->domains as $id => $domain) { + $this->findLink($domain->label()); + } + + // All affiliates link. + $this->drupalGet($url . '/all_affiliates'); + $this->assertResponse(403); + + // Individual domain pages. + foreach ($this->domains as $id => $domain) { + $this->drupalGet($url . '/' . $id); + $this->assertResponse(200); + } + } + + // This user should be able to see everything but all affiliates and nothing + // for editor assignments. + $this->editor_user = $this->drupalCreateUser([ + 'access administration pages', + 'access domain content', + 'publish to any assigned domain', + ]); + $this->addDomainsToEntity('user', $this->editor_user->id(), array_keys($this->domains), DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + $this->drupalLogin($this->editor_user); + // Test the overview and domain-specific pages. + foreach ($urls as $url) { + $expected = 200; + if ($url == 'admin/content/domain-editors') { + $expected = 403; + } + $this->drupalGet($url); + $this->assertResponse($expected); + // Find the links. + $this->assertNoRaw('All affiliates'); + foreach ($this->domains as $id => $domain) { + if ($expected == 200) { + $this->findLink($domain->label()); + } + else { + $this->findNoLink($domain->label()); + } + } + + // All affiliates link will fail for both paths. + $this->drupalGet($url . '/all_affiliates'); + $this->assertResponse(403); + + // Individual domain pages. + foreach ($this->domains as $id => $domain) { + $this->drupalGet($url . '/' . $id); + $this->assertResponse($expected); + } + } + + // This user should be able to see one domain for editor assignments. + $this->assign_user = $this->drupalCreateUser([ + 'access administration pages', + 'access domain content editors', + 'assign domain editors', + ]); + $ids = array_keys($this->domains); + $assigned_id = end($ids); + $this->addDomainsToEntity('user', $this->assign_user->id(), [$assigned_id], DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + $this->drupalLogin($this->assign_user); + // Test the overview and domain-specific pages. + foreach ($urls as $url) { + $expected = 200; + if ($url == 'admin/content/domain-content') { + $expected = 403; + } + $this->drupalGet($url); + $this->assertResponse($expected); + // Find the links. + $this->assertNoRaw('All affiliates'); + foreach ($this->domains as $id => $domain) { + if ($expected == 200 && $id == $assigned_id) { + $this->findLink($domain->label()); + } + else { + $this->findNoLink($domain->label()); + } + } + + // All affiliates link will fail for both paths. + $this->drupalGet($url . '/all_affiliates'); + $this->assertResponse(403); + + // Individual domain pages. + foreach ($this->domains as $id => $domain) { + $this->drupalGet($url . '/' . $id); + if ($expected == 200 && $id == $assigned_id) { + $this->assertResponse(200); + } + else { + $this->assertResponse(403); + } + } + } + } + +} diff --git a/domain_content/tests/src/Functional/DomainContentTestBase.php b/domain_content/tests/src/Functional/DomainContentTestBase.php new file mode 100644 index 00000000..21ce0ec0 --- /dev/null +++ b/domain_content/tests/src/Functional/DomainContentTestBase.php @@ -0,0 +1,103 @@ +domainCreateTestDomains(5); + + $this->domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + } + + /** + * Creates dummy content for testing. + * + * 25 nodes, 5 per domain and 5 to all affiliates. + */ + public function createDomainContent() { + foreach ($this->domains as $id => $domain) { + for ($i = 0; $i < 5; $i++) { + $this->drupalCreateNode([ + 'type' => 'article', + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => [$id], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => ($id == 'one_example_com') ? 1 : 0, + ]); + } + } + // Rebuild node access rules. + node_access_rebuild(); + } + + /** + * Creates dummy content for testing. + * + * 25 users, 5 per domain and 5 to all affiliates. + */ + public function createDomainUsers() { + foreach ($this->domains as $id => $domain) { + for ($i = 0; $i < 5; $i++) { + $account[$id] = $this->drupalCreateUser([ + 'access administration pages', + 'access domain content', + 'access domain content editors', + 'publish to any domain', + 'assign editors to any domain', + ]); + $this->addDomainsToEntity('user', $account[$id]->id(), $id, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + if ($id == 'one_example_com') { + $this->addDomainsToEntity('user', $account[$id]->id(), 1, DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD); + } + } + } + } + + /** + * Strips whitespace from a page response and runs assertRaw() equivalent. + * + * In tests, we were having difficulty with spacing in tables. This method + * takes some concepts from Mink and rearranges them to work for our tests. + * Notably, we don't pull page content from the session request. + * + * @param string $content + * The generated HTML, such as from drupalGet(). + * @param string $text + * The text string to search for. + */ + public function checkContent($content, $text) { + // Convert all whitespace to spaces. + $content = preg_replace('/\s+/u', ' ', $content); + // Strip all whitespace between tags. + $content = preg_replace('@>\\s+<@', '><', $content); + $regex = '/' . preg_quote($text, '/') . '/ui'; + $message = sprintf('The text "%s" was found in the text of the current page.', $text); + $this->assert((bool) preg_match($regex, $content), $message); + } + +} diff --git a/domain_source/README.md b/domain_source/README.md new file mode 100644 index 00000000..7508eeba --- /dev/null +++ b/domain_source/README.md @@ -0,0 +1,58 @@ +Domain Source +====== + +Allows content to be assigned a canonical domain when writing URLs. Domain Source will +ensure that content that appears on multiple domains always links to one URL. + +When links are written to content from another domain, the links +will go to the "source" domain specified for the node. + +Domain Source does not issue redirects. It rewrites links. The only time redirects may +be involved is when content is saved. + +Domain Source Fields +----- + +Domain Source adds a field to each content type that works with or without Domain Access. +The field is optional. If Domain Access is present, only a selected domain may be +assigned as the source domain. + +Domain Source configuration +----- + +The module has a configuration option that allows administrators to disable link rewrites +for specific Drupal paths. These paths are defined by Drupal's routing system. The default' +set of routes are: + +* `canonical` -- the View page of a node +* `delete_form` -- the delete form +* `edit_form` -- the edit form +* `version_history` -- the revisions page +* `revision` -- the revision edit form +* `create` -- not enforced + +Additional routes may be defined by other modules. Most notable Content Translation, +which adds: + +* `content_translation_overview` -- the overview page +* `content_translation_add` -- the add translation link +* `content_translation_edit` -- the edit translation form +* `content_translation_delete` -- the delete translation form + +Additional Entities +----- + +Domain Source is designed to work for content (nodes) but should work with other content +entities. You will need to configure the field for each entity type manually. + +Developer Notes +----- + +Domain Source changes core's `redirect_response_subscriber` service to the +`DomainSourceRedirectResponseSubscriber` class. This allows us to issue redirects to +registered domains and aliases that would otherwise not be recognizes as internal Drupal +links. These redirects typically occur on entity save when the source domain varies from +the current domain. + +Domain Source also implements `OutboundPathProcessorInterface` to rewrite links to an +entity assigned to a source domain. diff --git a/domain_source/config/install/domain_source.settings.yml b/domain_source/config/install/domain_source.settings.yml new file mode 100644 index 00000000..d63da0eb --- /dev/null +++ b/domain_source/config/install/domain_source.settings.yml @@ -0,0 +1 @@ +exclude_routes: {} diff --git a/domain_source/config/install/field.storage.node.field_domain_source.yml b/domain_source/config/install/field.storage.node.field_domain_source.yml index 11160116..9ae9937d 100644 --- a/domain_source/config/install/field.storage.node.field_domain_source.yml +++ b/domain_source/config/install/field.storage.node.field_domain_source.yml @@ -4,6 +4,9 @@ dependencies: module: - domain - node + enforced: + module: + - domain_source id: node.field_domain_source field_name: field_domain_source entity_type: node diff --git a/domain_source/config/schema/domain_source.schema.yml b/domain_source/config/schema/domain_source.schema.yml new file mode 100644 index 00000000..bbde9a45 --- /dev/null +++ b/domain_source/config/schema/domain_source.schema.yml @@ -0,0 +1,10 @@ +domain_source.settings: + type: config_object + label: 'Domain Source settings' + mapping: + exclude_routes: + type: sequence + label: 'Disable link rewrites for specific entity routes.' + sequence: + type: string + label: 'Route' diff --git a/domain_source/config/schema/domain_source.views.schema.yml b/domain_source/config/schema/domain_source.views.schema.yml new file mode 100644 index 00000000..5ce71d58 --- /dev/null +++ b/domain_source/config/schema/domain_source.views.schema.yml @@ -0,0 +1,9 @@ +views.field.domain_source: + type: views_field + label: 'Domain source' +views.filter.domain_source: + type: views.filter.in_operator + label: 'Domain source' +views.filter_value.domain_source: + type: views.filter_value.in_operator + label: 'Domain source' diff --git a/domain_source/domain_source.api.php b/domain_source/domain_source.api.php index 38ac4069..709c6b33 100644 --- a/domain_source/domain_source.api.php +++ b/domain_source/domain_source.api.php @@ -6,17 +6,20 @@ */ /** - * Allows modules to specify the target domain for a node. + * Allows modules to specify the target domain for an entity. * * There is no return value for this hook. Modify $source by reference by * loading a valid domain record or set $source = NULL to discard an existing - * $source value and not rewrite the path. * + * $source value and not rewrite the path. + * + * Note that $options['entity'] is the entity for the path request and + * $options['entity_type'] is the type of entity (e.g. 'node'). + * These values have already been verified before this hook is called. * - * Note that $options['entity'] is the node for the path request and - * $options['entity_type'] is the type of entity. These values have already - * been verified before this hook is called. + * If the entity's path is a translation, the requested translation of the + * entity will be passed as the $entity value. * - * @param \Drupal\domain\Entity\Domain|NULL &$source + * @param \Drupal\domain\Entity\Domain|null &$source * A domain object or NULL if not set. * @param string $path * The outbound path request. @@ -24,22 +27,20 @@ * The options for the url, as defined by * \Drupal\Core\PathProcessor\OutboundPathProcessorInterface. */ -function hook_domain_source_alter(&$source, $path, $options) { +function hook_domain_source_alter(array &$source, $path, array $options) { // Always link to the default domain. - $source = \Drupal::service('domain.loader')->loadDefaultDomain(); + $source = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultDomain(); } /** * Allows modules to specify the target link for a Drupal path. * - * Note: This hook is not meant to be used for node paths, which + * Note: This hook is not meant to be used for node or entity paths, which * are handled by hook_domain_source_alter(). This hook is split * from hook_domain_source_alter() for better performance. * - * Note that hook_domain_source_alter() only applies to nodes. It is possible - * that other entities may be passed here. If set, $options['entity'] is the - * entity for the path request and $options['entity_type'] is its type. - * These values have _not_ been verified before this hook is called. + * Note that hook_domain_source_alter() only paths that are not content + * entities. * * Currently, no modules in the package implement this hook. * @@ -55,10 +56,10 @@ function hook_domain_source_alter(&$source, $path, $options) { * The options for the url, as defined by * \Drupal\Core\PathProcessor\OutboundPathProcessorInterface. */ -function hook_domain_source_path_alter(&$source, $path, $options) { +function hook_domain_source_path_alter(array &$source, $path, array $options) { // Always make admin links go to the primary domain. $parts = explode('/', $path); if (isset($parts[0]) && $parts[0] == 'admin') { - $source = \Drupal::service('domain.loader')->loadDefaultDomain(); + $source = \Drupal::entityTypeManager()->getStorage('domain')->loadDefaultDomain(); } } diff --git a/domain_source/domain_source.info.yml b/domain_source/domain_source.info.yml index d4aa5f5b..ff980c1a 100644 --- a/domain_source/domain_source.info.yml +++ b/domain_source/domain_source.info.yml @@ -2,7 +2,14 @@ name: Domain Source description: 'Domain-based path rewrites for content.' type: module package: Domain -version: VERSION -core: 8.x +# version: VERSION +core_version_requirement: ^8 || ^9 dependencies: - - domain + - drupal:node + - drupal:path_alias + - domain:domain + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_source/domain_source.install b/domain_source/domain_source.install index 606fa36b..9bde50b5 100644 --- a/domain_source/domain_source.install +++ b/domain_source/domain_source.install @@ -12,8 +12,13 @@ * files because we have an unknown number of node types. */ function domain_source_install() { + if (\Drupal::isConfigSyncing()) { + // Configuration is assumed to already be checked by the config importer + // validation events. + return; + } // Assign domain source to bundles. - $list = array(); + $list = []; $node_types = \Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple(); foreach ($node_types as $type => $info) { $list[$type] = 'node'; @@ -23,16 +28,3 @@ function domain_source_install() { domain_source_confirm_fields($entity_type, $bundle); } } -/** - * Implements hook_uninstall(). - * - * Removes source domain fields on uninstall. - */ -function domain_source_uninstall() { - foreach (array('node') as $type) { - $id = $type . '.' . DOMAIN_SOURCE_FIELD; - if ($field = \Drupal::entityTypeManager()->getStorage('field_storage_config')->load($id)) { - $field->delete(); - } - } -} diff --git a/domain_source/domain_source.libraries.yml b/domain_source/domain_source.libraries.yml new file mode 100644 index 00000000..a61d45d8 --- /dev/null +++ b/domain_source/domain_source.libraries.yml @@ -0,0 +1,7 @@ +drupal.domain_source: + version: VERSION + js: + js/domain_source.js: {} + dependencies: + - core/jquery + - core/drupal diff --git a/domain_source/domain_source.links.menu.yml b/domain_source/domain_source.links.menu.yml new file mode 100644 index 00000000..e4086577 --- /dev/null +++ b/domain_source/domain_source.links.menu.yml @@ -0,0 +1,6 @@ +domain_source.settings: + title: Domain Source settings + route_name: domain_source.settings + parent: domain.admin + description: 'Domain Source settings' + weight: 15 diff --git a/domain_source/domain_source.links.task.yml b/domain_source/domain_source.links.task.yml new file mode 100644 index 00000000..a636149e --- /dev/null +++ b/domain_source/domain_source.links.task.yml @@ -0,0 +1,4 @@ +domain_source.settings: + title: Domain Source settings + route_name: domain_source.settings + base_route: domain.admin diff --git a/domain_source/domain_source.module b/domain_source/domain_source.module index 6a50633f..d96dfcba 100644 --- a/domain_source/domain_source.module +++ b/domain_source/domain_source.module @@ -8,9 +8,16 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\TrustedRedirectResponse; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\domain\DomainRedirectResponse; +use Drupal\domain_access\DomainAccessManagerInterface; +use Drupal\domain_source\DomainSourceElementManagerInterface; /** * Defines the name of the source domain field. + * + * @deprecated This constant will be replaced in the final release by + * Drupal\domain\DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD. */ const DOMAIN_SOURCE_FIELD = 'field_domain_source'; @@ -26,36 +33,42 @@ const DOMAIN_SOURCE_FIELD = 'field_domain_source'; * @see domain_source_install() */ function domain_source_confirm_fields($entity_type, $bundle) { - $id = $entity_type . '.' . $bundle . '.' . DOMAIN_SOURCE_FIELD; + $id = $entity_type . '.' . $bundle . '.' . DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD; $field_config_storage = \Drupal::entityTypeManager()->getStorage('field_config'); if (!$field = $field_config_storage->load($id)) { - $field = array( - 'field_name' => DOMAIN_SOURCE_FIELD, + $field = [ + 'field_name' => DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, 'entity_type' => $entity_type, 'label' => 'Domain Source', 'bundle' => $bundle, 'required' => FALSE, 'description' => 'Select the canonical domain for this content.', - 'settings' => array( - 'handler_settings' => array( - 'sort' => array('field' => 'weight', 'direction' => 'ASC'), - ), - ), - ); + 'settings' => [ + 'handler' => 'default:domain', + // Handler_settings are deprecated but seem to be necessary here. + 'handler_settings' => [ + 'target_bundles' => NULL, + 'sort' => ['field' => 'weight', 'direction' => 'ASC'], + ], + 'target_bundles' => NULL, + 'sort' => ['field' => 'weight', 'direction' => 'ASC'], + ], + ]; $field_config = $field_config_storage->create($field); $field_config->save(); } // Tell the form system how to behave. Default to radio buttons. - // @TODO: This function is deprecated, but using the OO syntax is causing - // test fails. - entity_get_form_display($entity_type, $bundle, 'default') - ->setComponent(DOMAIN_SOURCE_FIELD, array( + $display = \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load($entity_type . '.' . $bundle . '.default'); + if ($display) { + $display->setComponent(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, [ 'type' => 'options_select', - 'weight' => 40, - )) - ->save(); + 'weight' => 42, + ])->save(); + } } /** @@ -66,7 +79,24 @@ function domain_source_confirm_fields($entity_type, $bundle) { * @TODO: Make this possible for all entity types. */ function domain_source_node_type_insert(EntityInterface $entity) { - domain_source_confirm_fields('node', $entity->id()); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + if (!$entity->isSyncing()) { + // Do not fire hook when config sync in progress. + domain_source_confirm_fields('node', $entity->id()); + } +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + * + * In some cases, form display modes are not set when the node type is created. + * be sure to update our field definitions on creation of form_display for + * node types. + */ +function domain_source_entity_form_display_insert(EntityInterface $entity) { + if (!$entity->isSyncing() && $entity->getTargetEntityTypeId() == 'node' && $bundle = $entity->getTargetBundle()) { + domain_source_confirm_fields('node', $bundle); + } } /** @@ -75,15 +105,20 @@ function domain_source_node_type_insert(EntityInterface $entity) { * @param Drupal\Core\Entity\EntityInterface $entity * The entity to check. * - * @return string|NULL + * @return string|null * The value assigned to the entity, either a domain id string or NULL. */ function domain_source_get(EntityInterface $entity) { $source = NULL; - $value = $entity->get(DOMAIN_SOURCE_FIELD)->offsetGet(0); + + if (!isset($entity->{DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD})) { + return $source; + } + + $value = $entity->get(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD)->offsetGet(0); if (!empty($value)) { $target_id = $value->target_id; - if ($domain = \Drupal::service('domain.loader')->load($target_id)) { + if ($domain = \Drupal::entityTypeManager()->getStorage('domain')->load($target_id)) { $source = $domain->id(); } } @@ -99,7 +134,9 @@ function domain_source_get(EntityInterface $entity) { function domain_source_form_alter(&$form, &$form_state, $form_id) { $object = $form_state->getFormObject(); // Set up our TrustedRedirect handler for form saves. - if (isset($form[DOMAIN_SOURCE_FIELD]) && !empty($object) && is_callable([$object, 'getEntity']) && $entity = $object->getEntity()) { + if (isset($form[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]) && !empty($object) && is_callable([$object, 'getEntity']) && $entity = $object->getEntity()) { + // Validate the form. + $form['#validate'][] = 'domain_source_form_validate'; foreach ($form['actions'] as $key => $element) { // Redirect submit handlers, but not the preview button. if ($key != 'preview' && isset($element['#type']) && $element['#type'] == 'submit') { @@ -109,22 +146,59 @@ function domain_source_form_alter(&$form, &$form_state, $form_id) { } } +/** + * Validate form submissions. + */ +function domain_source_form_validate($element, FormStateInterface $form_state) { + $values = $form_state->getValues(); + // This is only run if Domain Access is present. + if (isset($values[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]) && \Drupal::moduleHandler()->moduleExists('domain_access') && isset($values[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD])) { + $access_values = $values[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD]; + $source_value = current($values[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]); + } + // If no value is selected, that's acceptable. Else run through a check. + // Note that the _none selection returns as [FALSE]. + $source_valid = FALSE; + if (empty($source_value)) { + $source_valid = TRUE; + } + else { + foreach ($access_values as $value) { + // Core is inconsistent depending on the field order. + // See https://www.drupal.org/project/domain/issues/2945771#comment-12493199 + if (is_array($value) && $value == $source_value) { + $source_valid = TRUE; + } + elseif (is_string($value) && !empty($source_value['target_id']) && $value == $source_value['target_id']) { + $source_valid = TRUE; + } + } + } + if (!$source_valid) { + $form_state->setError($element, t('The source domain must be selected as a publishing option.')); + } +} + /** * Redirect form submissions to other domains. */ -function domain_source_form_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) { +function domain_source_form_submit(&$form, FormStateInterface $form_state) { // Ensure that we have saved an entity. if ($object = $form_state->getFormObject()) { - $url = $object->getEntity()->url(); + $urlObject = $object->getEntity()->toUrl(); } // Validate that the URL will be considered "external" by Drupal, which means // that a scheme value will be present. - if (!empty($url)) { + if (!empty($urlObject)) { + $url = $urlObject->toString(); $uri_parts = parse_url($url); - // If necessary, issue a TrustedRedirectResponse to the new URL. - if (!empty($uri_parts['scheme'])) { - $response = new TrustedRedirectResponse($url); - $form_state->setResponse($response); + // If necessary and secure, issue a TrustedRedirectResponse to the new URL. + if (!empty($uri_parts['host'])) { + // Pass a redirect if necessary. + if (DomainRedirectResponse::checkTrustedHost($uri_parts['host'])) { + $response = new TrustedRedirectResponse($url); + $form_state->setResponse($response); + } } } } @@ -136,5 +210,117 @@ function domain_source_form_node_form_alter(&$form, FormStateInterface $form_sta // Add the options hidden from the user silently to the form. $manager = \Drupal::service('domain_source.element_manager'); $hide = TRUE; - $form = $manager->setFormOptions($form, $form_state, DOMAIN_SOURCE_FIELD, $hide); + $form = $manager->setFormOptions($form, $form_state, DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, $hide); + // If using a select field, load the JS to show/hide options. + if (isset($form[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]) && \Drupal::moduleHandler()->moduleExists('domain_access') && isset($form[DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD])) { + if ($form[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]['widget']['#type'] == 'select') { + $form['#attached']['library'][] = 'domain_source/drupal.domain_source'; + } + } +} + +/** + * Implements hook_views_data_alter(). + */ +function domain_source_views_data_alter(array &$data) { + $table = 'node__' . DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD; + $data[$table][DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]['field']['id'] = 'domain_source'; + $data[$table][DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD . '_target_id']['filter']['id'] = 'domain_source'; + // Since domains are not stored in the database, relationships cannot be used. + unset($data[$table][DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD]['relationship']); +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Add options for domain source when using Devel Generate. + */ +function domain_source_form_devel_generate_form_content_alter(&$form, &$form_state, $form_id) { + // Add our element to the Devel generate form. + $list = ['_derive' => t('Derive from domain selection')]; + $list += \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(); + $form['domain_source'] = [ + '#title' => t('Domain source'), + '#type' => 'checkboxes', + '#options' => $list, + '#weight' => 4, + '#multiple' => TRUE, + '#size' => count($list) > 5 ? 5 : count($list), + '#default_value' => ['_derive'], + '#description' => t('Sets the source domain for created nodes.'), + ]; +} + +/** + * Implements hook_ENTITY_TYPE_presave(). + * + * Fires only if Devel Generate module is present, to assign test nodes to + * domains. + */ +function domain_source_node_presave(EntityInterface $node) { + domain_source_presave_generate($node); +} + +/** + * Handles presave operations for devel generate. + */ +function domain_source_presave_generate(EntityInterface $entity) { + // Handle devel module settings. + $exists = \Drupal::moduleHandler()->moduleExists('devel_generate'); + $values = []; + $selections = []; + if ($exists && isset($entity->devel_generate)) { + // If set by the form. + if (isset($entity->devel_generate['domain_access'])) { + $selection = array_filter($entity->devel_generate['domain_access']); + if (isset($selection['random-selection'])) { + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $selections = array_rand($domains, ceil(rand(1, count($domains)))); + } + else { + $selections = array_keys($selection); + } + } + if (isset($entity->devel_generate['domain_source'])) { + $selection = $entity->devel_generate['domain_source']; + if ($selection == '_derive') { + if (!empty($selections)) { + $values[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD] = current($selections); + } + else { + $values[DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD] = NULL; + } + } + foreach ($values as $name => $value) { + $entity->set($name, $value); + } + } + } +} + +/** + * Implements hook_token_info(). + */ +function domain_source_token_info() { + return \Drupal::service('domain_source.token')->getTokenInfo(); +} + +/** + * Implements hook_tokens(). + */ +function domain_source_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + return \Drupal::service('domain_source.token')->getTokens($type, $tokens, $data, $options, $bubbleable_metadata); +} + +/** + * Implements hook_hook_info(). + */ +function domain_source_hook_info() { + $hooks['domain_source_alter'] = [ + 'group' => 'domain_source', + ]; + $hooks['domain_source_path_alter'] = [ + 'group' => 'domain_source', + ]; + return $hooks; } diff --git a/domain_source/domain_source.routing.yml b/domain_source/domain_source.routing.yml new file mode 100644 index 00000000..551e3740 --- /dev/null +++ b/domain_source/domain_source.routing.yml @@ -0,0 +1,7 @@ +domain_source.settings: + path: '/admin/config/domain/domain_source' + defaults: + _title: 'Domain Source settings' + _form: '\Drupal\domain_source\Form\DomainSourceSettingsForm' + requirements: + _permission: 'administer domains' diff --git a/domain_source/domain_source.services.yml b/domain_source/domain_source.services.yml index 85fcf018..c2df4aaa 100644 --- a/domain_source/domain_source.services.yml +++ b/domain_source/domain_source.services.yml @@ -1,11 +1,12 @@ services: domain_source.element_manager: class: Drupal\domain_source\DomainSourceElementManager - tags: - - { name: persist } - arguments: ['@domain.loader'] + arguments: ['@entity_type.manager'] domain_source.path_processor: class: Drupal\domain_source\HttpKernel\DomainSourcePathProcessor - arguments: ['@domain.loader', '@domain.negotiator', '@module_handler'] + arguments: ['@domain.negotiator', '@module_handler', '@entity_type.manager', '@path_alias.manager', '@config.factory'] tags: - - { name: path_processor_outbound, priority: 200 } + - { name: path_processor_outbound, priority: 90 } + domain_source.token: + class: Drupal\domain_source\DomainSourceToken + arguments: ['@config.factory'] diff --git a/domain_source/drush.services.yml b/domain_source/drush.services.yml new file mode 100644 index 00000000..6c115c94 --- /dev/null +++ b/domain_source/drush.services.yml @@ -0,0 +1,5 @@ +services: + domain_source.commands: + class: \Drupal\domain_source\Commands\DomainSourceCommands + tags: + - { name: drush.command } diff --git a/domain_source/js/domain_source.js b/domain_source/js/domain_source.js new file mode 100644 index 00000000..6e549663 --- /dev/null +++ b/domain_source/js/domain_source.js @@ -0,0 +1,59 @@ +/** + * @file + * Attaches behaviors for the Domain Source module. + * + * If Domain Access is present, we show/hide selected publishing domains. This approach + * currently only works with a select field. + */ +(function ($) { + + "use strict"; + + /** + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.domainSourceAllowed = { + attach: function () { + + // Get the initial setting so that it can be reset. + var initialOption = $("#edit-field-domain-source").val(); + + // Onload, fire initial show/hide. + getDomains(); + + // Get the domains selected by the domain access field. + function getDomains() { + var domains = new Array(); + $("#edit-field-domain-access :checked").each(function(index, obj) { + domains.push(obj.value); + }); + setOptions(domains); + } + + // Based on selected domains, show/hide the selection options. + function setOptions(domains) { + $("#edit-field-domain-source option").each(function(index, obj) { + if (jQuery.inArray(obj.value, domains) == -1 && obj.value != '_none') { + // If the current selection is removed, reset the selection to _none. + if ($("#edit-field-domain-source").val() == obj.value) { + $("#edit-field-domain-source").val('_none'); + } + $("#edit-field-domain-source option[value=" + obj.value + "]").hide(); + } + else { + $("#edit-field-domain-source option[value=" + obj.value + "]").show(); + // If we reselected the initial value, reset the select option. + if (obj.value == initialOption) { + $("#edit-field-domain-source").val(obj.value); + } + } + }); + } + + // When the selections change, recalculate the select options. + $( "#edit-field-domain-access" ).on( "change", getDomains ); + } + }; + +})(jQuery); diff --git a/domain_source/src/Commands/DomainSourceCommands.php b/domain_source/src/Commands/DomainSourceCommands.php new file mode 100644 index 00000000..60968be1 --- /dev/null +++ b/domain_source/src/Commands/DomainSourceCommands.php @@ -0,0 +1,80 @@ +getFieldEntities(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD); + + return $result; + } + + /** + * @hook option domain:delete + */ + public function deleteOptions(Command $command, AnnotationData $annotationData) { + $command->addOption( + 'source-assign', + '', + InputOption::VALUE_OPTIONAL, + 'Reassign content for Domain Source', + null + ); + } + + /** + * @hook on-event domain-delete + */ + public function domainSourceDomainDelete($target_domain, $options) { + // Run our own deletion routine here. + if (is_null($options['content-assign'])) { + $policy_content = 'prompt'; + } + if (!empty($options['content-assign'])) { + if (in_array($options['content-assign'], $this->reassignment_policies, TRUE)) { + $policy_content = $options['content-assign']; + } + } + + $delete_options = [ + 'entity_filter' => 'node', + 'policy' => $policy_content, + 'field' => DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, + ]; + + return $this->doReassign($target_domain, $delete_options); + } + +} diff --git a/domain_source/src/DomainSourceElementManager.php b/domain_source/src/DomainSourceElementManager.php index 6e7aaf8d..1f8f5cb4 100644 --- a/domain_source/src/DomainSourceElementManager.php +++ b/domain_source/src/DomainSourceElementManager.php @@ -2,28 +2,28 @@ namespace Drupal\domain_source; -use Drupal\domain\DomainLoaderInterface; -use Drupal\domain\DomainElementManager; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\domain\DomainElementManager; +use Drupal\domain_source\DomainSourceElementManagerInterface; /** * Checks the access status of entities based on domain settings. */ -class DomainSourceElementManager extends DomainElementManager { +class DomainSourceElementManager extends DomainElementManager implements DomainSourceElementManagerInterface { /** - * @inheritdoc + * {@inheritdoc} */ - public function disallowedOptions(FormStateInterface $form_state, $field) { + public function disallowedOptions(FormStateInterface $form_state, array $field) { $options = []; $info = $form_state->getBuildInfo(); $entity = $form_state->getFormObject()->getEntity(); - $entity_values = $entity->get(DOMAIN_SOURCE_FIELD)->offsetGet(0); + $entity_values = $entity->get(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD)->offsetGet(0); if (isset($field['widget']['#options']) && !empty($entity_values)) { $value = $entity_values->getValue('target_id'); $options = array_diff_key(array_flip($value), $field['widget']['#options']); } return array_keys($options); } + } diff --git a/domain_source/src/DomainSourceElementManagerInterface.php b/domain_source/src/DomainSourceElementManagerInterface.php new file mode 100644 index 00000000..3a76f72b --- /dev/null +++ b/domain_source/src/DomainSourceElementManagerInterface.php @@ -0,0 +1,17 @@ +getDefinition('redirect_response_subscriber'); + if ($this->getDrupalVersion() > 8) { + $definition->setClass('Drupal\domain_source\EventSubscriber\DomainSourceRedirectResponseSubscriber'); + } + else { + $definition->setClass('Drupal\domain_source\EventSubscriber\DomainSourceRedirectResponseSubscriberD8'); + } + } + + /** + * Determines the Drupal version. + * + * @return integer + * The core numberic version. + */ + private function getDrupalVersion() { + return (int) substr(\Drupal::VERSION, 0, 1); + } + +} diff --git a/domain_source/src/DomainSourceToken.php b/domain_source/src/DomainSourceToken.php new file mode 100644 index 00000000..bd186000 --- /dev/null +++ b/domain_source/src/DomainSourceToken.php @@ -0,0 +1,105 @@ +configFactory = $config_factory; + } + + /** + * Implements hook_token_info(). + */ + public function getTokenInfo() { + // Domain Source tokens. + $info['tokens']['node']['canonical-source-domain-url'] = [ + 'name' => $this->t('Canonical Source Domain URL'), + 'description' => $this->t("The canonical URL from the source domain for this node."), + 'type' => 'node', + ]; + + return $info; + } + + /** + * Implements hook_tokens(). + */ + public function getTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $replacements = []; + + // Based on the type, get the proper domain context. + switch ($type) { + case 'node': + foreach ($tokens as $name => $original) { + if ($name !== 'canonical-source-domain-url') { + continue; + } + if (!empty($data['node'])) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $data['node']; + $original = $tokens['canonical-source-domain-url']; + if (in_array('canonical', $this->getExcludedRoutes()) && $node->hasField('field_domain_source') && !$node->field_domain_source->isEmpty()) { + /** @var \Drupal\domain\Domain $sourceDomain */ + $sourceDomain = $node->field_domain_source->entity; + $url = $node->toUrl('canonical')->toString(); + $replacements[$original] = $sourceDomain->buildUrl($url); + $bubbleable_metadata->addCacheableDependency($sourceDomain); + } + else { + $replacements[$original] = $node->toUrl('canonical')->setAbsolute()->toString(); + } + } + } + break; + } + + return $replacements; + } + + /** + * Gets the settings for domain source path rewrites. + * + * @return array + * The settings for domain source path rewrites. + */ + public function getExcludedRoutes() { + if (!isset($this->excludedRoutes)) { + $config = $this->configFactory->get('domain_source.settings'); + $routes = $config->get('exclude_routes'); + if (is_array($routes)) { + $this->excludedRoutes = array_flip($routes); + } + else { + $this->excludedRoutes = []; + } + } + return $this->excludedRoutes; + } + +} diff --git a/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriber.php b/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriber.php new file mode 100644 index 00000000..ba0ff363 --- /dev/null +++ b/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriber.php @@ -0,0 +1,68 @@ +getResponse(); + if ($response instanceof RedirectResponse) { + $request = $event->getRequest(); + + // Let the 'destination' query parameter override the redirect target. + // If $response is already a SecuredRedirectResponse, it might reject the + // new target as invalid, in which case proceed with the old target. + $destination = $request->query->get('destination'); + if ($destination) { + // The 'Location' HTTP header must always be absolute. + $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost()); + try { + $response->setTargetUrl($destination); + } + catch (\InvalidArgumentException $e) { + } + } + + // Regardless of whether the target is the original one or the overridden + // destination, ensure that all redirects are safe. + if (!($response instanceof SecuredRedirectResponse)) { + try { + // SecuredRedirectResponse is an abstract class that requires a + // concrete implementation. Default to DomainRedirectResponse, which + // considers only redirects to sites registered via Domain. + $safe_response = DomainRedirectResponse::createFromRedirectResponse($response); + $safe_response->setRequestContext($this->requestContext); + } + catch (\InvalidArgumentException $e) { + // If the above failed, it's because the redirect target wasn't + // local. Do not follow that redirect. Display an error message + // instead. We're already catching one exception, so trigger_error() + // rather than throw another one. + // We don't throw an exception, because this is a client error rather + // than a server error. + $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.'; + trigger_error($message, E_USER_ERROR); + $safe_response = new Response($message, 400); + } + $event->setResponse($safe_response); + } + } + } + +} diff --git a/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriberD8.php b/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriberD8.php new file mode 100644 index 00000000..fad5e270 --- /dev/null +++ b/domain_source/src/EventSubscriber/DomainSourceRedirectResponseSubscriberD8.php @@ -0,0 +1,68 @@ +getResponse(); + if ($response instanceof RedirectResponse) { + $request = $event->getRequest(); + + // Let the 'destination' query parameter override the redirect target. + // If $response is already a SecuredRedirectResponse, it might reject the + // new target as invalid, in which case proceed with the old target. + $destination = $request->query->get('destination'); + if ($destination) { + // The 'Location' HTTP header must always be absolute. + $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost()); + try { + $response->setTargetUrl($destination); + } + catch (\InvalidArgumentException $e) { + } + } + + // Regardless of whether the target is the original one or the overridden + // destination, ensure that all redirects are safe. + if (!($response instanceof SecuredRedirectResponse)) { + try { + // SecuredRedirectResponse is an abstract class that requires a + // concrete implementation. Default to DomainRedirectResponse, which + // considers only redirects to sites registered via Domain. + $safe_response = DomainRedirectResponse::createFromRedirectResponse($response); + $safe_response->setRequestContext($this->requestContext); + } + catch (\InvalidArgumentException $e) { + // If the above failed, it's because the redirect target wasn't + // local. Do not follow that redirect. Display an error message + // instead. We're already catching one exception, so trigger_error() + // rather than throw another one. + // We don't throw an exception, because this is a client error rather + // than a server error. + $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.'; + trigger_error($message, E_USER_ERROR); + $safe_response = new Response($message, 400); + } + $event->setResponse($safe_response); + } + } + } + +} diff --git a/domain_source/src/Form/DomainSourceSettingsForm.php b/domain_source/src/Form/DomainSourceSettingsForm.php new file mode 100644 index 00000000..332529b5 --- /dev/null +++ b/domain_source/src/Form/DomainSourceSettingsForm.php @@ -0,0 +1,66 @@ +getDefinition('node')->getLinkTemplates(); + + $options = []; + foreach ($routes as $route => $path) { + // Some parts of the system prepend drupal:, which the routing + // system doesn't use. The routing system also uses underscores instead + // of dashes. Because Drupal. + $route = str_replace(['-', 'drupal:'], ['_', ''], $route); + $options[$route] = $route; + } + $config = $this->config('domain_source.settings'); + $form['exclude_routes'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Disable link rewrites for the selected routes.'), + '#default_value' => $config->get('exclude_routes') ?: [], + '#options' => $options, + '#description' => $this->t('Check the routes to disable. Any entity URL with a Domain Source field will be rewritten unless its corresponding route is disabled.'), + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('domain_source.settings') + ->set('exclude_routes', $form_state->getValue('exclude_routes')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/domain_source/src/HttpKernel/DomainSourcePathProcessor.php b/domain_source/src/HttpKernel/DomainSourcePathProcessor.php index 72356375..0f752ddc 100644 --- a/domain_source/src/HttpKernel/DomainSourcePathProcessor.php +++ b/domain_source/src/HttpKernel/DomainSourcePathProcessor.php @@ -2,11 +2,14 @@ namespace Drupal\domain_source\HttpKernel; -use Drupal\domain\DomainLoaderInterface; use Drupal\domain\DomainNegotiatorInterface; +use Drupal\path_alias\AliasManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Url; use Symfony\Component\HttpFoundation\Request; /** @@ -14,13 +17,6 @@ */ class DomainSourcePathProcessor implements OutboundPathProcessorInterface { - /** - * The Domain loader. - * - * @var \Drupal\domain\DomainLoaderInterface $loader - */ - protected $loader; - /** * The Domain negotiator. * @@ -35,56 +31,150 @@ class DomainSourcePathProcessor implements OutboundPathProcessorInterface { */ protected $moduleHandler; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The path alias manager. + * + * @var \Drupal\Core\Path\AliasManagerInterface + */ + protected $aliasManager; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * An array of content entity types. + * + * @var array + */ + protected $entityTypes; + + /** + * An array of routes exclusion settings, keyed by route. + * + * @var array + */ + protected $excludedRoutes; + + /** + * The active domain request. + * + * @var \Drupal\domain\DomainInterface + */ + protected $activeDomain; + + /** + * The domain storage. + * + * @var \Drupal\domain\DomainStorageInterface|null + */ + protected $domainStorage; + /** * Constructs a DomainSourcePathProcessor object. * - * @param \Drupal\domain\DomainLoaderInterface $loader - * The domain loader. * @param \Drupal\domain\DomainNegotiatorInterface $negotiator * The domain negotiator. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\path_alias\AliasManagerInterface $alias_manager + * The path alias manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. */ - public function __construct(DomainLoaderInterface $loader, DomainNegotiatorInterface $negotiator, ModuleHandlerInterface $module_handler) { - $this->loader = $loader; + public function __construct(DomainNegotiatorInterface $negotiator, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, AliasManagerInterface $alias_manager, ConfigFactoryInterface $config_factory) { $this->negotiator = $negotiator; $this->moduleHandler = $module_handler; + $this->entityTypeManager = $entity_type_manager; + $this->aliasManager = $alias_manager; + $this->configFactory = $config_factory; } /** * {@inheritdoc} */ - public function processOutbound($path, &$options = array(), Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { - static $active_domain; - - if (!isset($active_domain)) { - // Ensure that the loader has run. - // In some tests, the kernel event has not. - $active = \Drupal::service('domain.negotiator')->getActiveDomain(); - if (empty($active)) { - $active = \Drupal::service('domain.negotiator')->getActiveDomain(TRUE); - } - $active_domain = $active; + public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) { + // Load the active domain if not set. + if (empty($options['active_domain'])) { + $active_domain = $this->getActiveDomain(); } // Only act on valid internal paths and when a domain loads. if (empty($active_domain) || empty($path) || !empty($options['external'])) { return $path; } + + // Set the default source information. $source = NULL; $options['active_domain'] = $active_domain; - $entity = $this->getEntity($path, $options, 'node'); + // Get the current language. + $langcode = NULL; + if (!empty($options['language'])) { + $langcode = $options['language']->getId(); + } + + // Get the URL object for this request. + $alias = $this->aliasManager->getPathByAlias($path, $langcode); + $url = Url::fromUserInput($alias, $options); - // One hook for nodes. - if (!empty($entity)) { - if ($target_id = domain_source_get($entity)) { - $source = $this->loader->load($target_id); + // Get the route name to pass through to the alter hooks. + if ($url->isRouted()) { + $options['route_name'] = $url->getRouteName(); + } + + // Check the route, if available. Entities can be configured to + // only rewrite specific routes. + if ($url->isRouted() && $this->allowedRoute($url->getRouteName())) { + // Load the entity to check. + if (!empty($options['entity'])) { + $entity = $options['entity']; + } + else { + $parameters = $url->getRouteParameters(); + if (!empty($parameters)) { + $entity = $this->getEntity($parameters); + } } + } + + // One hook for entities. + if (!empty($entity) && is_object($entity)) { + // Ensure we send the right translation. + if (!empty($langcode) && method_exists($entity, 'hasTranslation') && $entity->hasTranslation($langcode) && $translation = $entity->getTranslation($langcode)) { + $entity = $translation; + } + if (isset($options['domain_target_id'])) { + $target_id = $options['domain_target_id']; + } + else { + $target_id = domain_source_get($entity); + } + if (!empty($target_id)) { + $source = $this->domainStorage()->load($target_id); + } + $options['entity'] = $entity; + $options['entity_type'] = $entity->getEntityTypeId(); $this->moduleHandler->alter('domain_source', $source, $path, $options); } // One for other, because the latter is resource-intensive. else { + if (isset($options['domain_target_id'])) { + $target_id = $options['domain_target_id']; + $source = $this->domainStorage()->load($target_id); + } $this->moduleHandler->alter('domain_source_path', $source, $path, $options); } // If a source domain is specified, rewrite the link. @@ -97,52 +187,113 @@ public function processOutbound($path, &$options = array(), Request $request = N } /** - * Derive entity data from a given path. + * Derive entity data from a given route's parameters. * - * @param $path - * The drupal path, e.g. /node/2. - * @param $options array - * The options passed to the path processor. - * @param $type - * The entity type to check. + * @param array $parameters + * An array of route parameters. * - * @return $entity|NULL + * @return \Drupal\Core\Entity\EntityInterface|null + * Returns the entity when available, otherwise NULL. */ - public static function getEntity($path, $options, $type = 'node') { + public function getEntity(array $parameters) { $entity = NULL; - if (isset($options['entity_type']) && $options['entity_type'] == $type) { - $entity = $options['entity']; + $entity_type = key($parameters); + $entity_types = $this->getEntityTypes(); + foreach ($parameters as $entity_type => $value) { + if (!empty($entity_type) && isset($entity_types[$entity_type])) { + $entity = $this->entityTypeManager->getStorage($entity_type)->load($value); + } } - elseif (isset($options['route'])) { - // Derive the route pattern and check that it maps to the expected entity - // type. - $route_path = $options['route']->getPath(); - $entityManager = \Drupal::entityTypeManager(); - $entityType = $entityManager->getDefinition($type); - $links = $entityType->getLinkTemplates(); - - // Check that the route pattern is an entity template. - if (in_array($route_path, $links)) { - $parts = explode('/', $route_path); - $i = 0; - foreach ($parts as $part) { - if (!empty($part)) { - $i++; - } - if ($part == '{' . $type . '}') { - break; - } - } - // Get Node path if alias. - $node_path = \Drupal::service('path.alias_manager')->getPathByAlias($path); - // Look! We're using arg() in Drupal 8 because we have to. - $args = explode('/', $node_path); - if (isset($args[$i])) { - $entity = \Drupal::entityTypeManager()->getStorage($type)->load($args[$i]); + return $entity; + } + + /** + * Checks that a route's common name is not disallowed. + * + * Looks at the name (e.g. canonical) of the route without regard for + * the entity type. + * + * @parameter $name + * The route name being checked. + * + * @return bool + * Returns TRUE when allowed, otherwise FALSE. + */ + public function allowedRoute($name) { + $excluded = $this->getExcludedRoutes(); + $parts = explode('.', $name); + $route_name = end($parts); + // Config is stored as an array. Empty items are not excluded. + return !isset($excluded[$route_name]); + } + + /** + * Gets an array of content entity types, keyed by type. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of content entity types, keyed by type. + */ + public function getEntityTypes() { + if (!isset($this->entityTypes)) { + foreach ($this->entityTypeManager->getDefinitions() as $type => $definition) { + if ($definition->getGroup() == 'content') { + $this->entityTypes[$type] = $type; } } } - return $entity; + return $this->entityTypes; + } + + /** + * Gets the settings for domain source path rewrites. + * + * @return array + * The settings for domain source path rewrites. + */ + public function getExcludedRoutes() { + if (!isset($this->excludedRoutes)) { + $config = $this->configFactory->get('domain_source.settings'); + $routes = $config->get('exclude_routes'); + if (is_array($routes)) { + $this->excludedRoutes = array_flip($routes); + } + else { + $this->excludedRoutes = []; + } + } + return $this->excludedRoutes; + } + + /** + * Gets the active domain. + * + * @return \Drupal\domain\DomainInterface + * The active domain. + */ + public function getActiveDomain() { + if (!isset($this->activeDomain)) { + // Ensure that the loader has run. + // In some tests, the kernel event has not. + $active = $this->negotiator->getActiveDomain(); + if (empty($active)) { + $active = $this->negotiator->getActiveDomain(TRUE); + } + $this->activeDomain = $active; + } + return $this->activeDomain; + } + + /** + * Retrieves the domain storage handler. + * + * @return \Drupal\domain\DomainStorageInterface + * The domain storage handler. + */ + protected function domainStorage() { + if (!$this->domainStorage) { + $this->domainStorage = $this->entityTypeManager->getStorage('domain'); + } + return $this->domainStorage; } } diff --git a/domain_source/src/Plugin/views/field/DomainSource.php b/domain_source/src/Plugin/views/field/DomainSource.php new file mode 100644 index 00000000..7bfa5722 --- /dev/null +++ b/domain_source/src/Plugin/views/field/DomainSource.php @@ -0,0 +1,50 @@ +options['settings']['link'])) { + foreach ($items as &$item) { + $object = $item['raw']; + $entity = $object->getEntity(); + $url = $entity->toUrl()->toString(); + $domain = $item['rendered']['#options']['entity']; + $item['rendered']['#type'] = 'markup'; + $item['rendered']['#markup'] = '' . $domain->label() . ''; + } + uasort($items, [$this, 'sort']); + } + + return $items; + } + + /** + * Sort the domain list, if possible. + */ + private function sort($a, $b) { + $domainA = isset($a['rendered']['#options']['entity']) ? $a['rendered']['#options']['entity'] : 0; + $domainB = isset($b['rendered']['#options']['entity']) ? $b['rendered']['#options']['entity'] : 0; + if ($domainA !== 0) { + return ($domainA->getWeight() > $domainB->getWeight()) ? 1 : 0; + } + // We don't have a domain object so sort as best we can. + return strcmp($a['rendered']['#plain_text'], $b['rendered']['#plain_text']); + } + +} diff --git a/domain_source/src/Plugin/views/filter/DomainSource.php b/domain_source/src/Plugin/views/filter/DomainSource.php new file mode 100644 index 00000000..1983bfbf --- /dev/null +++ b/domain_source/src/Plugin/views/filter/DomainSource.php @@ -0,0 +1,42 @@ +valueOptions)) { + $this->valueTitle = $this->t('Domains'); + $this->valueOptions = [ + '_active' => $this->t('Active domain'), + ] + \Drupal::entityTypeManager()->getStorage('domain')->loadOptionsList(); + } + return $this->valueOptions; + } + + /** + * {@inheritdoc} + */ + public function query() { + $active_index = array_search('_active', (array) $this->value); + if ($active_index !== FALSE) { + $active_id = \Drupal::service('domain.negotiator')->getActiveId(); + $this->value[$active_index] = $active_id; + } + + parent::query(); + } + +} diff --git a/domain_source/tests/modules/domain_source_test/config/optional/views.view.domain_format_test.yml b/domain_source/tests/modules/domain_source_test/config/optional/views.view.domain_format_test.yml new file mode 100644 index 00000000..098153fb --- /dev/null +++ b/domain_source/tests/modules/domain_source_test/config/optional/views.view.domain_format_test.yml @@ -0,0 +1,212 @@ +langcode: en +status: true +dependencies: + module: + - node + - rest + - serialization + - user +id: domain_format_test +label: 'Domain format test' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 2 + display_options: + display_extenders: { } + path: domain-format-test + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + rest_export_1: + display_plugin: rest_export + id: rest_export_1 + display_title: 'REST export' + position: 1 + display_options: + display_extenders: { } + path: domain-format-test + row: + type: data_field + options: + field_options: + title: + alias: '' + raw_output: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - request_format + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/domain_source/tests/modules/domain_source_test/domain_source_test.info.yml b/domain_source/tests/modules/domain_source_test/domain_source_test.info.yml new file mode 100644 index 00000000..0616ac02 --- /dev/null +++ b/domain_source/tests/modules/domain_source_test/domain_source_test.info.yml @@ -0,0 +1,20 @@ +name: "Domain Source module tests" +description: "Support module for domain source testing." +type: module +package: Testing +# version: VERSION +core_version_requirement: ^8 || ^9 +hidden: TRUE + +dependencies: + - domain + - domain_source + - node + - rest + - serialization + - user + +# Information added by Drupal.org packaging script on 2021-06-24 +version: '8.x-1.0-beta6' +project: 'domain' +datestamp: 1624563601 diff --git a/domain_source/tests/modules/domain_source_test/domain_source_test.module b/domain_source/tests/modules/domain_source_test/domain_source_test.module new file mode 100644 index 00000000..624ec3f6 --- /dev/null +++ b/domain_source/tests/modules/domain_source_test/domain_source_test.module @@ -0,0 +1,13 @@ +getStorage('domain')->loadDefaultDomain(); + } +} diff --git a/domain_source/tests/src/Functional/DomainSourceContentUrlsTest.php b/domain_source/tests/src/Functional/DomainSourceContentUrlsTest.php new file mode 100644 index 00000000..658d9f32 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceContentUrlsTest.php @@ -0,0 +1,85 @@ + 'page', + 'title' => 'foo', + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => ['example_com', 'one_example_com', 'two_example_com'], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 0, + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id, + ]; + $node = $this->createNode($nodes_values); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $source = $domains[$id]; + $expected = $source->getPath() . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertTrue($url == $expected, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUri'); + + // Get the path processor service. + $paths = \Drupal::service('domain_access.manager'); + $urls = $paths->getContentUrls($node); + $expected = [ + $id => $domains[$id]->getPath() . 'node/1', + 'example_com' => $domains['example_com']->getPath() . 'node/1', + 'two_example_com' => $domains['two_example_com']->getPath() . 'node/1', + ]; + + $this->assertTrue($expected == $urls); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceElementTest.php b/domain_source/tests/src/Functional/DomainSourceElementTest.php new file mode 100644 index 00000000..61d2b01d --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceElementTest.php @@ -0,0 +1,186 @@ +domainCreateTestDomains(5); + } + + /** + * Test runner. + */ + public function testDomainSourceElement() { + $this->runInstalledTest('article'); + $node_type = $this->createContentType(['type' => 'test']); + $this->runInstalledTest('test'); + } + + /** + * Basic test setup. + */ + public function runInstalledTest($node_type) { + $admin = $this->drupalCreateUser([ + 'bypass node access', + 'administer content types', + 'administer node fields', + 'administer node display', + 'administer domains', + 'publish to any domain', + ]); + $this->drupalLogin($admin); + + $this->drupalGet('node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + $nid = $node_type == 'article' ? 1 : 2; + + // Set the title, so the node can be saved. + $this->fillField('title[0][value]', 'Test node'); + + // We expect to find 5 domain options. We set two as selected. + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $count = 0; + $ids = ['example_com', 'one_example_com', 'two_example_com']; + foreach ($domains as $domain) { + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD . '[' . $domain->id() . ']'; + $this->findField($locator); + if (in_array($domain->id(), $ids)) { + $this->checkField($locator); + } + } + // Find the all affiliates field. + $locator = DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD . '[value]'; + $this->findField($locator); + + // Set all affiliates to TRUE. + $this->checkField($locator); + + // Find the Domain Source field. + $locator = DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD; + $this->findField($locator); + // Set it to one_example_com. + $this->selectFieldOption($locator, 'one_example_com'); + + // Save the form. + $this->pressButton('edit-submit'); + $this->assertSession()->statusCodeEquals(200); + + // Check the URL. + $url = $this->geturl(); + $this->assert(strpos($url, 'node/' . $nid . '/edit') === FALSE, 'Form submitted.'); + + // Edit the node. + $this->drupalGet('node/' . $nid . '/edit'); + $this->assertSession()->statusCodeEquals(200); + + // Set the domain source field to an unselected domain. + $this->selectFieldOption($locator, 'three_example_com'); + + // Save the form. + $this->pressButton('edit-submit'); + $this->assertSession()->pageTextContains('The source domain must be selected as a publishing option.'); + + // Check the URL. + $url = $this->geturl(); + $this->assert(strpos($url, 'node/' . $nid . '/edit') > 0, 'Form not submitted.'); + + // Set the field properly and save again. + $this->selectFieldOption($locator, 'one_example_com'); + + // Save the form. + $this->pressButton('edit-submit'); + $this->assertSession()->statusCodeEquals(200); + + // Check the URL. + $url = $this->geturl(); + $this->assert(strpos($url, 'node/' . $nid . '/edit') === FALSE, 'Form submitted.'); + + // Save with no source. + // Edit the node. + $this->drupalGet('node/1/edit'); + $this->assertSession()->statusCodeEquals(200); + + // Set the domain source field to an unselected domain. + $this->selectFieldOption($locator, '_none'); + + // Save the form. + $this->pressButton('edit-submit'); + $this->assertSession()->statusCodeEquals(200); + + // Check the URL. + $url = $this->geturl(); + $this->assert(strpos($url, 'node/' . $nid . '/edit') === FALSE, 'Form submitted.'); + } + + /** + * Test for https://www.drupal.org/project/domain/issues/3010256. + */ + public function testAnonForm() { + // Editor with no domain permissions should not see the element. + $editor = $this->drupalCreateUser([ + 'create article content', + ]); + $this->drupalLogin($editor); + + $this->drupalGet('node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + $locator = DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD; + $this->assertSession()->fieldNotExists($locator); + + // Editor with domain permissions should see the element once they + // are assigned to domains. + $editor2 = $this->drupalCreateUser([ + 'create article content', + 'publish to any assigned domain', + ]); + $this->drupalLogin($editor2); + + $this->drupalGet('node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + $locator = DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD; + $this->assertSession()->fieldNotExists($locator); + + // Domain assignment. + $ids = ['example_com', 'one_example_com']; + $this->addDomainsToEntity('user', $editor2->id(), $ids, DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD); + + $this->drupalGet('node/add/article'); + $this->assertSession()->statusCodeEquals(200); + + $this->assertSession()->fieldExists($locator); + } + +} diff --git a/domain_source/src/Tests/DomainSourceEntityReferenceTest.php b/domain_source/tests/src/Functional/DomainSourceEntityReferenceTest.php similarity index 64% rename from domain_source/src/Tests/DomainSourceEntityReferenceTest.php rename to domain_source/tests/src/Functional/DomainSourceEntityReferenceTest.php index f7d8a7eb..b4fb67d4 100644 --- a/domain_source/src/Tests/DomainSourceEntityReferenceTest.php +++ b/domain_source/tests/src/Functional/DomainSourceEntityReferenceTest.php @@ -1,9 +1,8 @@ admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'administer content types', 'administer node fields', 'administer node display', 'administer domains', - )); + ]); $this->drupalLogin($this->admin_user); // Visit the article field administration page. @@ -62,13 +56,13 @@ public function testDomainSourceNodeField() { * Tests the storage of the domain source field. */ public function testDomainSourceFieldStorage() { - $this->admin_user = $this->drupalCreateUser(array( + $this->admin_user = $this->drupalCreateUser([ 'administer content types', 'administer node fields', 'administer node display', 'administer domains', 'administer menu', - )); + ]); $this->drupalLogin($this->admin_user); // Create 5 domains. @@ -82,10 +76,10 @@ public function testDomainSourceFieldStorage() { $this->assertText('Domain Source', 'Found the domain field instance.'); // We expect to find 5 domain options + none. - $domains = \Drupal::service('domain.loader')->loadMultiple(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); foreach ($domains as $domain) { $string = 'value="' . $domain->id() . '"'; - $this->assertRaw($string, new FormattableMarkup('Found the %domain option.', array('%domain' => $domain->label()))); + $this->assertRaw($string, 'Found the domain option.'); if (!isset($one)) { $one = $domain->id(); continue; @@ -102,22 +96,24 @@ public function testDomainSourceFieldStorage() { // Try to post a node, assigned to the second domain. $edit['title[0][value]'] = 'Test node'; $edit['field_domain_source'] = $two; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertResponse(200); $node = $node_storage->load(1); // Check that the value is set. $value = domain_source_get($node); - $this->assertTrue($value == $two, 'Node saved with proper source record.'); + $this->assertEquals($two, $value, 'Node saved with proper source record.'); // Test the URL. $url = $node->toUrl()->toString(); $expected_url = $two_path . 'node/1'; - $this->assertTrue($expected_url == $url, 'URL rewritten correctly.'); + $this->assertEquals($expected_url, $url, 'URL rewritten correctly.'); // Try to post a node, assigned to no domain. $edit['title[0][value]'] = 'Test node'; $edit["field_domain_source"] = '_none'; - $this->drupalPostForm('node/add/article', $edit, 'Save'); + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); $this->assertResponse(200); $node = $node_storage->load(2); // Check that the value is set. @@ -127,34 +123,57 @@ public function testDomainSourceFieldStorage() { // Test the url. $url = $node->toUrl()->toString(); $expected_url = base_path() . 'node/2'; - $this->assertTrue($expected_url == $url, 'URL rewritten correctly.'); + $this->assertEquals($expected_url, $url, 'URL rewritten correctly.'); // Place the menu block. $this->drupalPlaceBlock('system_menu_block:main'); // Enable main menu as available menu. - $edit = array( + $edit = [ 'menu_options[main]' => 1, 'menu_parent' => 'main:', - ); - $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type')); + ]; + $this->drupalGet('admin/structure/types/manage/article'); + $this->submitForm($edit, 'Save content type'); // Create a third node that is assigned to a menu. - $edit = array( + $edit = [ 'title[0][value]' => 'Node 3', 'menu[enabled]' => 1, 'menu[title]' => 'Test preview', 'field_domain_source' => $two, - ); - $this->drupalPostForm('node/add/article', $edit, 'Save'); + ]; + $this->drupalGet('node/add/article'); + $this->submitForm($edit, 'Save'); // Test the URL against expectations, and the rendered menu link. $node = $node_storage->load(3); $url = $node->toUrl()->toString(); $expected_url = $two_path . 'node/3'; - $this->assertTrue($expected_url == $url, 'URL rewritten correctly.'); + $this->assertEquals($expected_url, $url, 'URL rewritten correctly.'); // Load the page with a menu and check that link. $this->drupalGet('node/3'); $this->assertRaw('href="' . $url, 'Menu link rewritten correctly.'); + + // Remove the field from the node type and make sure nothing breaks. + // See https://www.drupal.org/node/2892612 + $id = 'node.article.field_domain_source'; + if ($field = \Drupal::entityTypeManager()->getStorage('field_config')->load($id)) { + $field->delete(); + field_purge_batch(10, $field->uuid()); + drupal_flush_all_caches(); + } + // Visit the article field display administration page. + $this->drupalGet('node/add/article'); + $this->assertResponse(200); + // Try to post a node, assigned to no domain. + $edit2['title[0][value]'] = 'Test node'; + $this->drupalGet('node/add/article'); + $this->submitForm($edit2, 'Save'); + // Test the URL against expectations, and the rendered menu link. + $node = $node_storage->load(4); + $url = $node->toUrl()->toString(); + $expected_url = base_path() . 'node/4'; + $this->assertEquals($expected_url, $url, 'No URL rewrite performed.'); } } diff --git a/domain_source/tests/src/Functional/DomainSourceExcludeTest.php b/domain_source/tests/src/Functional/DomainSourceExcludeTest.php new file mode 100644 index 00000000..0d818906 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceExcludeTest.php @@ -0,0 +1,99 @@ + 'page', + 'title' => 'foo', + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id, + ]; + $node = $this->createNode($node_values); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $source = $domains[$id]; + $expected = $source->getPath() . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertEquals($expected, $url, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUri'); + + // Exclude the edit path from rewrites. + $config = $this->config('domain_source.settings'); + $config->set('exclude_routes', ['edit_form' => 'edit_form'])->save(); + + // Variables for our tests. + $path = 'node/1/edit'; + $expected = base_path() . $path; + $route_name = 'entity.node.edit_form'; + $route_parameters = ['node' => 1]; + $uri = 'internal:/' . $path; + $uri_path = '/' . $path; + $options = []; + + // Because of path cache, we have to flush here. + drupal_flush_all_caches(); + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertEquals($expected, $url, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUri'); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceLanguageTest.php b/domain_source/tests/src/Functional/DomainSourceLanguageTest.php new file mode 100644 index 00000000..b828e783 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceLanguageTest.php @@ -0,0 +1,128 @@ +save(); + ConfigurableLanguage::createFromLangcode('af')->save(); + + // Enable content translation for the current entity type. + \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); + + } + + /** + * Tests domain source language. + */ + public function testDomainSourceLanguage() { + // Create a node, assigned to a source domain. + $id = 'one_example_com'; + // Create one node with no language. + $node = $this->drupalCreateNode([ + 'body' => [[]], + 'status' => 1, + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id, + ]); + + // Programmatically create a translation. + $storage = \Drupal::entityTypeManager()->getStorage('node'); + // Reload the node. + $node = $storage->load(1); + // Create an Afrikaans translation assigned to domain 2. + $id2 = 'two_example_com'; + $translation = $node->addTranslation('af'); + $translation->title->value = $this->randomString(); + $translation->{DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD} = $id2; + $translation->status = 1; + $node->save(); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $source = $domains[$id]; + $expected = $source->getPath() . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertTrue($url == $expected, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUri'); + + // Now test the same for the Arfrikaans translation. + $path = 'node/1'; + $source = $domains[$id2]; + $expected = $source->getPath() . 'af/' . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $language = \Drupal::entityTypeManager()->getStorage('configurable_language')->load('af'); + $options = ['language' => $language]; + + $translation = $node->getTranslation('af'); + $this->assertTrue(domain_source_get($translation) == $id2, domain_source_get($translation)); + + // Because of path cache, we have to flush here. + drupal_flush_all_caches(); + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertTrue($url == $expected, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUri'); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceParameterTest.php b/domain_source/tests/src/Functional/DomainSourceParameterTest.php new file mode 100644 index 00000000..9c009449 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceParameterTest.php @@ -0,0 +1,55 @@ +createNode(['type' => 'page', 'title' => 'foo', DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id]); + + // Variables for our tests. + $path = 'domain-format-test'; + $options = ['query' => ['_format' => 'json']]; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + foreach ($domains as $domain) { + $this->drupalGet($domain->getPath() . $path, $options); + } + $source = $domains[$id]; + $uri_path = '/' . $path; + $expected = base_path() . $path . '?_format=json'; + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertEquals($expected, $url, 'fromUserInput'); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceTokenTest.php b/domain_source/tests/src/Functional/DomainSourceTokenTest.php new file mode 100644 index 00000000..b94dc503 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceTokenTest.php @@ -0,0 +1,75 @@ +domainCreateTestDomains(4, 'example.com'); + } + + /** + * Tests domain source tokens. + */ + public function testDomainSourceTokens() { + $token_handler = \Drupal::token(); + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + // Create a node, assigned to a source domain. + $nodes_values = [ + 'type' => 'page', + 'title' => 'foo', + DomainAccessManagerInterface::DOMAIN_ACCESS_FIELD => ['example_com', 'one_example_com', 'two_example_com'], + DomainAccessManagerInterface::DOMAIN_ACCESS_ALL_FIELD => 0, + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => 'one_example_com', + ]; + $node = $this->createNode($nodes_values); + + // Token value matches the normal canonical url when canonical rewrite is used. + $this->assertEqual($token_handler->replace('[node:canonical-source-domain-url]', ['node' => $node]), $domains['one_example_com']->getPath() . 'node/1'); + $this->assertEqual($node->toUrl('canonical')->setAbsolute()->toString(), $domains['one_example_com']->getPath() . 'node/1'); + + $node->set(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, 'two_example_com'); + $this->assertEqual($token_handler->replace('[node:canonical-source-domain-url]', ['node' => $node]), $domains['two_example_com']->getPath() . 'node/1'); + $this->assertEqual($node->toUrl('canonical')->setAbsolute()->toString(), $domains['two_example_com']->getPath() . 'node/1'); + + // Exclude the canonical path from rewrites. + $config = $this->config('domain_source.settings'); + $config->set('exclude_routes', ['canonical' => 'canonical'])->save(); + // Because of path cache, we have to flush here. + drupal_flush_all_caches(); + + // Test token value, and URL without token. + $node->set(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, 'one_example_com'); + $one_example_com_absolute_url = $node->toUrl('canonical')->setAbsolute()->toString(); + $this->assertEqual($token_handler->replace('[node:canonical-source-domain-url]', ['node' => $node]), $domains['one_example_com']->getPath() . 'node/1'); + + $node->set(DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD, 'two_example_com'); + $two_example_com_absolute_url = $node->toUrl('canonical')->setAbsolute()->toString(); + $this->assertEqual($token_handler->replace('[node:canonical-source-domain-url]', ['node' => $node]), $domains['two_example_com']->getPath() . 'node/1'); + + $this->assertEqual($one_example_com_absolute_url, $two_example_com_absolute_url, 'Canonical url rewrite is not used, domain source change did not affect url.'); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceTrustedHostTest.php b/domain_source/tests/src/Functional/DomainSourceTrustedHostTest.php new file mode 100644 index 00000000..5b5449a9 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceTrustedHostTest.php @@ -0,0 +1,85 @@ + 'page', + 'title' => 'foo', + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id, + ]; + $node = $this->createNode($node_values); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $source = $domains[$id]; + $expected = $source->getPath() . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertTrue($url == $expected, 'fromRoute'); + + // Set up two additional domains. + $domain2 = $domains['two_example_com']; + + // Check against trusted host patterns. + $settings['settings']['trusted_host_patterns'] = (object) [ + 'value' => ['^' . $this->prepareTrustedHostname($domain2->getHostname()) . '$'], + 'required' => TRUE, + ]; + $this->writeSettings($settings); + // This URL should fail due to trusted host omission. + $this->drupalGet($url); + $this->assertRaw('The provided host name is not valid for this server.'); + + // Now switch the node to a domain that is trusted. + $node->{DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD} = $domain2->id(); + $node->save(); + // Get the link using Url::fromRoute(). + $expected = $domain2->getPath() . $path; + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + // Assert that the URL is what we expect. + $this->assertTrue($url == $expected, 'fromRoute'); + $this->drupalGet($url); + $this->assertResponse(200, 'Url is validated by trusted host settings.'); + } + +} diff --git a/domain_source/tests/src/Functional/DomainSourceUrlTest.php b/domain_source/tests/src/Functional/DomainSourceUrlTest.php new file mode 100644 index 00000000..d2e45268 --- /dev/null +++ b/domain_source/tests/src/Functional/DomainSourceUrlTest.php @@ -0,0 +1,71 @@ + 'page', + 'title' => 'foo', + DomainSourceElementManagerInterface::DOMAIN_SOURCE_FIELD => $id, + ]; + $node = $this->createNode($nodes_values); + + // Variables for our tests. + $path = 'node/1'; + $domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple(); + $source = $domains[$id]; + $expected = $source->getPath() . $path; + $route_name = 'entity.node.canonical'; + $route_parameters = ['node' => 1]; + $uri = 'entity:' . $path; + $uri_path = '/' . $path; + $options = []; + + // Get the link using Url::fromRoute(). + $url = Url::fromRoute($route_name, $route_parameters, $options)->toString(); + $this->assertTrue($url == $expected, 'fromRoute'); + + // Get the link using Url::fromUserInput() + $url = Url::fromUserInput($uri_path, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUserInput'); + + // Get the link using Url::fromUri() + $url = Url::fromUri($uri, $options)->toString(); + $this->assertTrue($url == $expected, 'fromUri'); + } + +} diff --git a/drupalci.yml b/drupalci.yml new file mode 100644 index 00000000..83fc544c --- /dev/null +++ b/drupalci.yml @@ -0,0 +1,21 @@ +build: + assessment: + validate_codebase: + phplint: + csslint: + halt-on-fail: false + eslint: + halt-on-fail: false + phpcs: + sniff-all-files: false + halt-on-fail: false + testing: + container_command: + commands: + - '/bin/bash -c "cd ${SOURCE_DIR}/modules/contrib/domain && chmod +x ./define_subdomains.sh && ./define_subdomains.sh"' + run_tests.standard: + types: 'PHPUnit-Unit,PHPUnit-Build,PHPUnit-Kernel,PHPUnit-Functional' + run_tests.js: + concurrency: 1 + types: 'PHPUnit-FunctionalJavascript' + nightwatchjs: