├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── doc ├── .gitignore ├── Makefile ├── _static │ └── snippets │ │ ├── composer.bash │ │ ├── composer.json │ │ ├── phpunit-composer.json │ │ ├── phpunitDrupalDriver.php │ │ ├── usage-blackbox.php │ │ ├── usage-drupal.php │ │ └── usage-drush.php ├── conf.py ├── drivers.rst ├── index.rst ├── install.rst ├── make.bat └── usage.rst ├── docker-compose.yml ├── phpcs-ruleset.xml ├── phpunit.xml.dist ├── spec └── Drupal │ └── Driver │ ├── BlackboxDriverSpec.php │ ├── Cores │ ├── Drupal6Spec.php │ ├── Drupal7Spec.php │ └── Drupal8Spec.php │ └── Exception │ ├── BootstrapExceptionSpec.php │ └── UnsupportedDriverActionExceptionSpec.php ├── src └── Drupal │ └── Driver │ ├── AuthenticationDriverInterface.php │ ├── BaseDriver.php │ ├── BlackboxDriver.php │ ├── Cores │ ├── AbstractCore.php │ ├── CoreAuthenticationInterface.php │ ├── CoreInterface.php │ ├── Drupal6.php │ ├── Drupal7.php │ └── Drupal8.php │ ├── DriverInterface.php │ ├── DrupalDriver.php │ ├── DrushDriver.php │ ├── Exception │ ├── BootstrapException.php │ ├── Exception.php │ └── UnsupportedDriverActionException.php │ ├── Fields │ ├── Drupal6 │ │ └── TaxonomyHandler.php │ ├── Drupal7 │ │ ├── AbstractHandler.php │ │ ├── DatetimeHandler.php │ │ ├── DefaultHandler.php │ │ ├── EntityreferenceHandler.php │ │ ├── FileHandler.php │ │ ├── ImageHandler.php │ │ ├── LinkFieldHandler.php │ │ ├── ListBooleanHandler.php │ │ ├── ListTextHandler.php │ │ └── TaxonomyTermReferenceHandler.php │ ├── Drupal8 │ │ ├── AbstractHandler.php │ │ ├── AddressHandler.php │ │ ├── DatetimeHandler.php │ │ ├── DefaultHandler.php │ │ ├── EmbridgeAssetItemHandler.php │ │ ├── EntityReferenceHandler.php │ │ ├── FileHandler.php │ │ ├── ImageHandler.php │ │ ├── LinkHandler.php │ │ ├── ListFloatHandler.php │ │ ├── ListHandlerBase.php │ │ ├── ListIntegerHandler.php │ │ ├── ListStringHandler.php │ │ ├── TaxonomyTermReferenceHandler.php │ │ └── TextWithSummaryHandler.php │ └── FieldHandlerInterface.php │ └── SubDriverFinderInterface.php └── tests └── Drupal └── Tests └── Driver ├── Drupal7FieldHandlerTest.php ├── DrushDriverTest.php └── FieldHandlerAbstractTestBase.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jhedstrom] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - php_version: "8.2" 11 | drupal_version: "10" 12 | - php_version: "8.3" 13 | drupal_version: "10" 14 | - php_version: "8.3" 15 | drupal_version: "11" 16 | env: 17 | PHP_VERSION: ${{ matrix.php_version }} 18 | DRUPAL_VERSION: ${{ matrix.drupal_version }} 19 | DOCKER_USER_ID: "1001" 20 | steps: 21 | - name: clone 22 | uses: actions/checkout@v3 23 | - name: docker-compose up -d 24 | run: docker compose up -d 25 | - name: composer self-update 26 | run: docker compose exec -T php composer self-update 27 | - name: composer require 28 | run: docker compose exec -u ${DOCKER_USER_ID} -T php composer require --no-interaction --dev --no-update drupal/core:^${DRUPAL_VERSION} drupal/core-recommended:^${DRUPAL_VERSION} 29 | - name: composer install 30 | run: docker compose exec -T php composer install 31 | - name: composer phpunit-configuration 32 | run: docker compose exec -T php phpunit --migrate-configuration 33 | if: "${{ matrix.drupal_version == '11'}}" 34 | - name: composer test 35 | run: docker compose exec -T php composer test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.tgz 3 | *.phar 4 | composer.lock 5 | reports 6 | vendor 7 | drush 8 | /drupal 9 | 10 | .phpunit.result.cache 11 | /.editorconfig 12 | /.gitattributes 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | ## [2.2.2] 10 | ### Added 11 | * [#258](https://github.com/jhedstrom/DrupalDriver/pull/258) Document testing of the Drupal Driver locally for contributors. 12 | ### Fixed 13 | * [#260](https://github.com/jhedstrom/DrupalDriver/pull/260) Fix taxonomy term creation for hierarchies. 14 | ## [2.2.1] 15 | * [#250](https://github.com/jhedstrom/DrupalDriver/pull/250) Drupal 10 compatibility. 16 | ## [2.2.0] 17 | ### Fixed 18 | * [#232](https://github.com/jhedstrom/DrupalDriver/pull/232) Fixes typo in ImageHandler. 19 | * [#233](https://github.com/jhedstrom/DrupalDriver/pull/233) Fixes Drupal 7.79 compatibility. 20 | * [#244](https://github.com/jhedstrom/DrupalDriver/pull/244) Drupal Coder updates. 21 | ### Added 22 | * [#245](https://github.com/jhedstrom/DrupalDriver/pull/245) PHP 8.1 compatibility. 23 | * [#247](https://github.com/jhedstrom/DrupalDriver/issues/247) Drupal 10 and PHP 8.1 compatibility. 24 | ## [2.1.1] 25 | ### Fixed 26 | * [#233](https://github.com/jhedstrom/DrupalDriver/pull/233) Prevent PHP warning in Drupal 7.79 and above. 27 | * [#232](https://github.com/jhedstrom/DrupalDriver/pull/232) Fix type in ImageHandler. 28 | ## [2.1.0] 29 | ### Added 30 | * [#186](https://github.com/jhedstrom/DrupalDriver/issues/186) Provide a method to directly authenticate on Drupal 8. 31 | ### Changed 32 | * Remove testing on PHP 5.6, added testing on PHP 7.3 and 7.4. 33 | * [#214](https://github.com/jhedstrom/DrupalDriver/pull/214) Fix D8 deprectations. 34 | * [#224](https://github.com/jhedstrom/DrupalDriver/pull/224) Support Drupal 9. 35 | ### Fixed 36 | * [#217](https://github.com/jhedstrom/DrupalDriver/pull/217) Fix fatal error in field handler. 37 | * [#219](https://github.com/jhedstrom/DrupalDriver/issues/219) Fix php notice in exception message. 38 | ## [2.0.0] 2019-09-27 39 | ## [2.0.0 rc1] 2019-07-25 40 | ### Changed 41 | * [#207](https://github.com/jhedstrom/DrupalDriver/pull/207) Require PHP 5.6 or higher. 42 | ## [2.0.0 alpha6] 2018-09-21 43 | ### Added 44 | * [#190](https://github.com/jhedstrom/DrupalDriver/pull/190) Added Drush entity support. 45 | * [#168](https://github.com/jhedstrom/DrupalDriver/issues/168) Added ListHandlers for Drupal 8. 46 | ### Changed 47 | * [#203](https://github.com/jhedstrom/DrupalDriver/pull/203) Removes testing of HHVM. 48 | ## [2.0.0 alpha5] 2018-09-21 49 | ### Fixed 50 | * [#199](https://github.com/jhedstrom/DrupalDriver/pull/199): Fixes type 51 | introduced in #198. 52 | ## [2.0.0 alpha4] 2018-09-21 53 | ### Added 54 | * [#191](https://github.com/jhedstrom/DrupalDriver/pull/191): Adds field 55 | handler for address fields. 56 | * [#196](https://github.com/jhedstrom/DrupalDriver/pull/196): Add a 57 | storeOriginalConfiguration method. 58 | * [#197](https://github.com/jhedstrom/DrupalDriver/pull/197): Added a method 59 | configGetOriginal which will return the original config data. 60 | ### Fixed 61 | * [#193](https://github.com/jhedstrom/DrupalDriver/pull/193): Fixing the 62 | ListTextHandler to allow a key to also be 0 63 | * [#198](https://github.com/jhedstrom/DrupalDriver/pull/198): Use 64 | cache:rebuild instead of cache-clear all with Drush 9. 65 | ## [2.0.0 alpha3] 2018-06-21 66 | ### Added 67 | * [#89](https://github.com/jhedstrom/DrupalDriver/pull/89): Adds Embridge asset 68 | item field handler. 69 | * [#184](https://github.com/jhedstrom/DrupalDriver/pull/184): Extract and store 70 | uid in DrushDriver::userCreate() 71 | * [#185](https://github.com/jhedstrom/DrupalDriver/pull/185): Support for 72 | deleting any entity, not just content entities. 73 | ## [2.0.0 alpha2] 2018-03-21 74 | ### Added 75 | * [#126](https://github.com/jhedstrom/DrupalDriver/pull/126): Infer timezone 76 | in DateTime field handler. 77 | * [#180](https://github.com/jhedstrom/DrupalDriver/pull/180): Support email 78 | collection when the Mail System module is enabled. 79 | ### Fixed 80 | * [#182](https://github.com/jhedstrom/DrupalDriver/pull/182): Persist mail 81 | collection to config storage so email collection works across bootstraps. 82 | 83 | ## [2.0.0 alpha1] 2018-03-19 84 | ### Added 85 | * [#113](https://github.com/jhedstrom/DrupalDriver/pull/113): Drupal 7 entity 86 | create/delete support. 87 | * [#114](https://github.com/jhedstrom/DrupalDriver/pull/114): Base field 88 | expansion. 89 | * [#134](https://github.com/jhedstrom/DrupalDriver/pull/134): Support for 90 | email testing. 91 | ### Changed 92 | * [#173](https://github.com/jhedstrom/DrupalDriver/pull/173): HHVM failures 93 | allowed, and newer versions of PHPSpec supported. 94 | ### Fixed 95 | * [#170](https://github.com/jhedstrom/DrupalDriver/pull/170): Missing methods 96 | added to `DriverInterface`. 97 | 98 | ## [1.4.0] 2018-02-09 99 | ### Added 100 | * [#136](https://github.com/jhedstrom/DrupalDriver/pull/136): Allows relative 101 | date formats. 102 | ### Changed 103 | * [#159](https://github.com/jhedstrom/DrupalDriver/pull/159): Ignore access on 104 | Drupal 8 entity reference handler. 105 | * [#162](https://github.com/jhedstrom/DrupalDriver/pull/162): Remove duplicate 106 | copy of core's `Random` class. 107 | * [#163](https://github.com/jhedstrom/DrupalDriver/pull/163): Remove PHP 5.4 108 | support and test on PHP 7.1 and 7.2. 109 | ### Fixed 110 | * [#117](https://github.com/jhedstrom/DrupalDriver/pull/117): Fix user entity 111 | reference fields in Drupal 8. 112 | * [#149](https://github.com/jhedstrom/DrupalDriver/pull/149): Fix condition to 113 | get target bundle key for entity reference handler. 114 | * [#151](https://github.com/jhedstrom/DrupalDriver/pull/151): Illegal string 115 | offset warnings. 116 | * [#153](https://github.com/jhedstrom/DrupalDriver/pull/153): Fix incorrect 117 | docblock for `CoreInterface::roleCreate`. 118 | 119 | 120 | [Unreleased]: https://github.com/jhedstrom/DrupalDriver/compare/v2.2.2...HEAD 121 | [2.2.2]: https://github.com/jhedstrom/DrupalDriver/compare/v2.2.1...v2.2.2 122 | [2.2.1]: https://github.com/jhedstrom/DrupalDriver/compare/v2.2.0...v2.2.1 123 | [2.2.0]: https://github.com/jhedstrom/DrupalDriver/compare/v2.1.1...v2.2.0 124 | [2.1.1]: https://github.com/jhedstrom/DrupalDriver/compare/v2.1.0...v2.1.1 125 | [2.1.0]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0...v2.1.0 126 | [2.0.0]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-rc1...v2.0.0 127 | [2.0.0 rc1]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha6...v2.0.0-rc1 128 | [2.0.0 alpha6]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha5...HEAD 129 | [2.0.0 alpha5]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha4...v2.0.0-alpha5 130 | [2.0.0 alpha4]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha3...v2.0.0-alpha4 131 | [2.0.0 alpha3]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha2...v2.0.0-alpha3 132 | [2.0.0 alpha2]: https://github.com/jhedstrom/DrupalDriver/compare/v2.0.0-alpha1...v2.0.0-alpha2 133 | [2.0.0 alpha1]: https://github.com/jhedstrom/DrupalDriver/compare/v1.4.0...v2.0.0-alpha1 134 | [1.4.0]: https://github.com/jhedstrom/DrupalDriver/compare/v1.3.2...v1.4.0 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Features and bug fixes are welcome! First-time contributors can jump in with the issues tagged [good first issue](https://github.com/jhedstrom/DrupalDriver/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). 4 | 5 | ## Testing 6 | 7 | Testing is performed automatically in Github Actions when a PR is submitted. To execute tests locally before submitting a PR, you'll need [Docker and Docker Compose](https://docs.docker.com/engine/install/). 8 | 9 | Configure your test environment: 10 | 11 | ``` 12 | export PHP_VERSION=8.3 13 | export DRUPAL_VERSION=11 14 | export DOCKER_USER_ID=${UID} 15 | ``` 16 | 17 | Prepare environment for testing: 18 | 19 | ``` 20 | docker compose up -d 21 | docker compose exec -T php composer self-update 22 | docker compose exec -u ${DOCKER_USER_ID} -T php composer require --no-interaction --dev --no-update drupal/core:^${DRUPAL_VERSION} 23 | docker compose exec -T php composer install 24 | ``` 25 | 26 | Execute all tests: 27 | 28 | ``` 29 | docker compose exec -T php composer test 30 | ``` 31 | 32 | Execute specific tests, eg just PHPUnit's Drupal7FieldHandlerTest: 33 | 34 | ``` 35 | docker compose exec -T php phpunit --filter Drupal7FieldHandlerTest 36 | ``` 37 | 38 | - Check the changes from `composer require` are not included in your submitted PR. 39 | - Before testing another PHP or Drupal version, remove `composer.lock` and `vendor/` 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | [![Build Status](https://github.com/jhedstrom/DrupalDriver/actions/workflows/ci.yml/badge.svg)](https://github.com/jhedstrom/DrupalDriver/actions/workflows/ci.yml) 8 | 9 | Provides a collection of light-weight drivers with a common interface for interacting with [Drupal](http://drupal.org). These are generally intended for testing, and are not meant to be API-complete. 10 | 11 | [Read the full documentation](http://drupal-drivers.readthedocs.org) 12 | 13 | [![Latest Stable Version](https://poser.pugx.org/drupal/drupal-driver/v/stable.svg)](https://packagist.org/packages/drupal/drupal-driver) [![Total Downloads](https://poser.pugx.org/drupal/drupal-driver/downloads.svg)](https://packagist.org/packages/drupal/drupal-driver) [![License](https://poser.pugx.org/drupal/drupal-driver/license.svg)](https://packagist.org/packages/drupal/drupal-driver) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jhedstrom/DrupalDriver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jhedstrom/DrupalDriver/?branch=master) 14 | 15 | ### Drivers 16 | 17 | These drivers support Drupal versions 7 and 8. 18 | 19 | * Blackbox 20 | * Direct Drupal API bootstrap 21 | * Drush 22 | 23 | ### Installation 24 | 25 | ``` json 26 | { 27 | "require": { 28 | "drupal/drupal-driver": "~2.0" 29 | } 30 | } 31 | ``` 32 | 33 | ``` bash 34 | $> curl -sS http://getcomposer.org/installer | php 35 | $> php composer.phar install 36 | ``` 37 | 38 | ### Usage 39 | 40 | ``` php 41 | setCoreFromVersion(); 55 | 56 | // Bootstrap Drupal. 57 | $driver->bootstrap(); 58 | 59 | // Create a node. 60 | $node = (object) array( 61 | 'type' => 'article', 62 | 'uid' => 1, 63 | 'title' => $driver->getRandom()->name(), 64 | ); 65 | $driver->createNode($node); 66 | ``` 67 | 68 | ### Contributing 69 | 70 | Features and bug fixes are welcome! First-time contributors can jump in with the 71 | issues tagged [good first issue](https://github.com/jhedstrom/DrupalDriver/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). 72 | 73 | See [CONTRIBUTING.md](https://github.com/jhedstrom/DrupalDriver/blob/master/CONTRIBUTING.md) for more information. 74 | 75 | ### Release notes 76 | 77 | See [CHANGELOG](CHANGELOG.MD). 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/drupal-driver", 3 | "type": "library", 4 | "description": "A collection of reusable Drupal drivers", 5 | "keywords": ["drupal", "web", "test"], 6 | "homepage": "http://github.com/jhedstrom/DrupalDriver", 7 | "license": "GPL-2.0-or-later", 8 | "authors": [ 9 | { 10 | "name": "Jonathan Hedstrom", 11 | "email": "jhedstrom@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.4", 16 | "symfony/process": "~2.5 || ~3.0 || ~4.4 || ^6 || ^7", 17 | "symfony/dependency-injection": "~2.6 || ~3.0 || ~4.4 || ^6 || ^7.1", 18 | "drupal/core-utility": "^8.4 || ^9 || ^10 || ^11" 19 | }, 20 | "require-dev": { 21 | "composer/installers": "^2.1", 22 | "drupal/coder": "~8.3.0", 23 | "phpspec/phpspec": "~2.0 || ~4.0 || ~6.1 || ~7.5 || dev-main", 24 | "phpunit/phpunit": "~6.0 || ~7.0 || ^9 || ^10", 25 | "mockery/mockery": "^1.5", 26 | "drupal/core-composer-scaffold": "^8.4 || ^9 || ^10 || ^11", 27 | "drupal/core-recommended": "^8.4 || ^9 || ^10 || ^11", 28 | "drupal/mailsystem": "^4.4 || 4.x-dev", 29 | "drush-ops/behat-drush-endpoint": "*", 30 | "php-parallel-lint/php-parallel-lint": "^1.0", 31 | "dms/phpunit-arraysubset-asserts": "^0.4.0 || ^0.5.0", 32 | "palantirnet/drupal-rector": "^0.20", 33 | "symfony/phpunit-bridge": "^6.1" 34 | }, 35 | "conflict": { 36 | "drupal/core": ">=8.0 <9.3" 37 | }, 38 | "scripts": { 39 | "lint": "XDEBUG_MODE=off parallel-lint src spec tests", 40 | "phpunit": "XDEBUG_MODE=coverage phpunit", 41 | "phpspec": "XDEBUG_MODE=off phpspec run -f pretty --no-interaction", 42 | "phpcs": "XDEBUG_MODE=off phpcs --standard=./phpcs-ruleset.xml .", 43 | "rector": [ 44 | "cp ./vendor/palantirnet/drupal-rector/rector.php drupal/.", 45 | "XDEBUG_MODE=off cd drupal && ../vendor/bin/rector process ../src/Drupal/Driver/Cores/Drupal8.php --dry-run", 46 | "XDEBUG_MODE=off cd drupal && ../vendor/bin/rector process ../src/Drupal/Driver/Fields/Drupal8 --dry-run" 47 | ], 48 | "test": [ 49 | "XDEBUG_MODE=off composer validate --no-interaction", 50 | "@lint", 51 | "@phpunit", 52 | "@phpspec", 53 | "@phpcs", 54 | "@rector" 55 | ] 56 | }, 57 | "autoload": { 58 | "psr-0": { 59 | "Drupal\\Driver": "src/", 60 | "Drupal\\Tests\\Driver" : "tests/" 61 | } 62 | }, 63 | "repositories": { 64 | "drupal": { 65 | "type": "composer", 66 | "url": "https://packages.drupal.org/8" 67 | } 68 | }, 69 | "prefer-stable": true, 70 | "minimum-stability": "beta", 71 | "extra": { 72 | "branch-alias": { 73 | "dev-master": "2.3.x-dev" 74 | }, 75 | "installer-paths": { 76 | "drupal/core": [ 77 | "type:drupal-core" 78 | ], 79 | "drupal/modules/{$name}": [ 80 | "type:drupal-module" 81 | ] 82 | }, 83 | "drupal-scaffold": { 84 | "locations": { 85 | "web-root": "drupal/" 86 | } 87 | } 88 | }, 89 | "config": { 90 | "allow-plugins": { 91 | "dealerdirect/phpcodesniffer-composer-installer": true, 92 | "drupal/core-composer-scaffold": true, 93 | "composer/installers": true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DrupalDrivers.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DrupalDrivers.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DrupalDrivers" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DrupalDrivers" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/_static/snippets/composer.bash: -------------------------------------------------------------------------------- 1 | $> curl -sS http://getcomposer.org/installer | php 2 | $> php composer.phar install 3 | -------------------------------------------------------------------------------- /doc/_static/snippets/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "drupal/drupal-driver": "~1.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /doc/_static/snippets/phpunit-composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "aik099/phpunit-mink": "~2.0", 4 | "drupal/drupal-driver": "~1.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /doc/_static/snippets/phpunitDrupalDriver.php: -------------------------------------------------------------------------------- 1 | 'localhost', 25 | 'port' => 4444, 26 | 'browserName' => 'firefox', 27 | 'baseUrl' => 'http://d8.devl', 28 | ), 29 | ); 30 | 31 | public static function setUpBeforeClass() { 32 | self::$driver = new DrupalDriver(static::$drupalRoot, static::$uri); 33 | self::$driver->setCoreFromVersion(); 34 | self::$driver->bootstrap(); 35 | } 36 | 37 | public function testUsingSession() 38 | { 39 | // This is Mink's Session. 40 | $session = $this->getSession(); 41 | 42 | // Go to a page. 43 | $session->visit(static::$uri); 44 | 45 | // Validate text presence on a page. 46 | $this->assertTrue($session->getPage()->hasContent('Site-Install')); 47 | } 48 | 49 | public function testUsingBrowser() 50 | { 51 | // Prints the name of used browser. 52 | echo sprintf( 53 | "I'm executed using '%s' browser", 54 | $this->getBrowser()->getBrowserName() 55 | ); 56 | } 57 | 58 | public function testNodeCreate() { 59 | $drupal = self::$driver; 60 | $node = (object) [ 61 | 'title' => $drupal->getRandom()->string(), 62 | 'type' => 'article', 63 | ]; 64 | $drupal->createNode($node); 65 | 66 | $session = $this->getSession(); 67 | $session->visit(static::$uri . '/node/' . $node->nid); 68 | 69 | $this->assertTrue($session->getPage()->hasContent($node->title)); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /doc/_static/snippets/usage-blackbox.php: -------------------------------------------------------------------------------- 1 | 'article', 14 | 'uid' => 1, 15 | 'title' => $driver->getRandom()->name(), 16 | ); 17 | $driver->createNode($node); 18 | } 19 | catch (UnsupportedDriverActionException $e) { 20 | // Mark test as skipped. 21 | } 22 | 23 | -------------------------------------------------------------------------------- /doc/_static/snippets/usage-drupal.php: -------------------------------------------------------------------------------- 1 | setCoreFromVersion(); 16 | 17 | // Bootstrap Drupal. 18 | $driver->bootstrap(); 19 | 20 | // Create a node. 21 | $node = (object) array( 22 | 'type' => 'article', 23 | 'uid' => 1, 24 | 'title' => $driver->getRandom()->name(), 25 | ); 26 | $driver->createNode($node); 27 | -------------------------------------------------------------------------------- /doc/_static/snippets/usage-drush.php: -------------------------------------------------------------------------------- 1 | v documentation". 122 | #html_title = None 123 | 124 | # A shorter title for the navigation bar. Default is the same as html_title. 125 | #html_short_title = None 126 | 127 | # The name of an image file (relative to this directory) to place at the top 128 | # of the sidebar. 129 | #html_logo = None 130 | 131 | # The name of an image file (within the static path) to use as favicon of the 132 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 133 | # pixels large. 134 | #html_favicon = None 135 | 136 | # Add any paths that contain custom static files (such as style sheets) here, 137 | # relative to this directory. They are copied after the builtin static files, 138 | # so a file named "default.css" will overwrite the builtin "default.css". 139 | html_static_path = ['_static'] 140 | 141 | # Add any extra paths that contain custom files (such as robots.txt or 142 | # .htaccess) here, relative to this directory. These files are copied 143 | # directly to the root of the documentation. 144 | #html_extra_path = [] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'DrupalDriversdoc' 189 | 190 | 191 | # -- Options for LaTeX output --------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | #'papersize': 'letterpaper', 196 | 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #'pointsize': '10pt', 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #'preamble': '', 202 | } 203 | 204 | # Grouping the document tree into LaTeX files. List of tuples 205 | # (source start file, target name, title, 206 | # author, documentclass [howto, manual, or own class]). 207 | latex_documents = [ 208 | ('index', 'DrupalDrivers.tex', u'Drupal Drivers Documentation', 209 | u'Jonathan Hedstrom', 'manual'), 210 | ] 211 | 212 | # The name of an image file (relative to this directory) to place at the top of 213 | # the title page. 214 | #latex_logo = None 215 | 216 | # For "manual" documents, if this is true, then toplevel headings are parts, 217 | # not chapters. 218 | #latex_use_parts = False 219 | 220 | # If true, show page references after internal links. 221 | #latex_show_pagerefs = False 222 | 223 | # If true, show URL addresses after external links. 224 | #latex_show_urls = False 225 | 226 | # Documents to append as an appendix to all manuals. 227 | #latex_appendices = [] 228 | 229 | # If false, no module index is generated. 230 | #latex_domain_indices = True 231 | 232 | 233 | # -- Options for manual page output --------------------------------------- 234 | 235 | # One entry per manual page. List of tuples 236 | # (source start file, name, description, authors, manual section). 237 | man_pages = [ 238 | ('index', 'drupaldrivers', u'Drupal Drivers Documentation', 239 | [u'Jonathan Hedstrom'], 1) 240 | ] 241 | 242 | # If true, show URL addresses after external links. 243 | #man_show_urls = False 244 | 245 | 246 | # -- Options for Texinfo output ------------------------------------------- 247 | 248 | # Grouping the document tree into Texinfo files. List of tuples 249 | # (source start file, target name, title, author, 250 | # dir menu entry, description, category) 251 | texinfo_documents = [ 252 | ('index', 'DrupalDrivers', u'Drupal Drivers Documentation', 253 | u'Jonathan Hedstrom', 'DrupalDrivers', 'One line description of project.', 254 | 'Miscellaneous'), 255 | ] 256 | 257 | # Documents to append as an appendix to all manuals. 258 | #texinfo_appendices = [] 259 | 260 | # If false, no module index is generated. 261 | #texinfo_domain_indices = True 262 | 263 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 264 | #texinfo_show_urls = 'footnote' 265 | 266 | # If true, do not generate a @detailmenu in the "Top" node's menu. 267 | #texinfo_no_detailmenu = False 268 | -------------------------------------------------------------------------------- /doc/drivers.rst: -------------------------------------------------------------------------------- 1 | Comparison of Drivers 2 | ===================== 3 | 4 | The available drivers for interacting with your site, which are 5 | compatible with Drupal 7, and 8. Each driver has its own limitiations. 6 | 7 | +-----------------------+----------+-------+------------+ 8 | | Feature | Blackbox | Drush | Drupal API | 9 | +=======================+==========+=======+============+ 10 | | Create users | No | Yes | Yes | 11 | +-----------------------+----------+-------+------------+ 12 | | Create nodes | No | [*] | Yes | 13 | +-----------------------+----------+-------+------------+ 14 | | Create vocabularies | No | No | Yes | 15 | +-----------------------+----------+-------+------------+ 16 | | Create taxonomy terms | No | [*] | Yes | 17 | +-----------------------+----------+-------+------------+ 18 | | Run tests and site | | | | 19 | | on different servers | Yes | Yes | No | 20 | +-----------------------+----------+-------+------------+ 21 | 22 | [*] Possible if behat.d7.drush.inc or behat.d8.drush.inc, 23 | as appropriate, is installed in the target Drupal site. 24 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Drupal Drivers documentation master file, created by 2 | sphinx-quickstart on Thu Oct 30 12:57:48 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Drupal Driver' documentation! 7 | ======================================== 8 | 9 | The `Drupal Drivers`_ are a collection of light-weight drivers with a common interface for interacting with Drupal_. These are generally intended for testing, and are not meant to be API-complete. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | 16 | install 17 | drivers 18 | usage 19 | 20 | .. _Drupal: http://drupal.org 21 | .. _`Drupal Drivers`: https://github.com/jhedstrom/DrupalDriver 22 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | To utilize the Drupal Drivers in your own project, they are installed via composer_. 5 | 6 | .. literalinclude:: _static/snippets/composer.json 7 | :language: json 8 | 9 | and then install and run composer 10 | 11 | .. literalinclude:: _static/snippets/composer.bash 12 | :language: bash 13 | 14 | .. _composer: https://getcomposer.org/ 15 | 16 | If you plan on using the Drush driver, then you need to ensure 17 | that the behat-drush-endpoint is available in the target Drupal 18 | site. There are two ways to do this: 19 | 20 | 1. Copy the files manually. The project can be found at: 21 | 22 | https://github.com/drush-ops/behat-drush-endpoint 23 | 24 | 2. Use Composer. 25 | 26 | If you are using Composer to manage your Drupal site, then 27 | you only need to require drupal/drupal-driver and 28 | composer/installers, and the behat-drush-endpoint files 29 | will be copied to the correct location. 30 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DrupalDrivers.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DrupalDrivers.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Drupal API driver 5 | ----------------- 6 | 7 | .. literalinclude:: _static/snippets/usage-drupal.php 8 | :language: php 9 | :linenos: 10 | :emphasize-lines: 14-15 11 | 12 | Drush driver 13 | ------------ 14 | 15 | .. literalinclude:: _static/snippets/usage-drush.php 16 | :language: php 17 | :linenos: 18 | :emphasize-lines: 7-8 19 | 20 | Blackbox 21 | -------- 22 | 23 | Note, the blackbox driver has no ability to control Drupal, and is provided as a fallback for when some tests can run without such access. 24 | 25 | Any testing application should catch unsupported driver exceptions. 26 | 27 | .. literalinclude:: _static/snippets/usage-blackbox.php 28 | :language: php 29 | :linenos: 30 | :emphasize-lines: 8,19 31 | 32 | Practical example with PHPUnit 33 | ------------------------------ 34 | 35 | By using the phpunit/mink project in conjunction with the Drupal Driver, one can use PHPUnit to drive browser sessions and control Drupal. 36 | 37 | To install: 38 | 39 | .. literalinclude:: _static/snippets/phpunit-composer.json 40 | :language: json 41 | :linenos: 42 | 43 | and then, in the tests directory, a sample test: 44 | 45 | .. literalinclude:: _static/snippets/phpunitDrupalDriver.php 46 | :language: php 47 | :linenos: 48 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | 4 | php: 5 | image: wodby/drupal-php:${PHP_VERSION} 6 | environment: 7 | PHP_FPM_USER: wodby 8 | PHP_FPM_GROUP: wodby 9 | PHP_FPM_CLEAR_ENV: "yes" 10 | PHP_OPCACHE_PRELOAD_USER: wodby 11 | PHP_XDEBUG_MODE: "on" 12 | PHP_XDEBUG_DEFAULT_ENABLE: 1 13 | PHP_XDEBUG_REMOTE_CONNECT_BACK: 1 14 | PHP_XDEBUG_REMOTE_HOST: "10.254.254.254" 15 | PHP_XDEBUG_IDEKEY: "PHPSTORM" 16 | PHP_IDE_CONFIG: "serverName=drupaldriver" 17 | volumes: 18 | - ./:/var/www/html 19 | -------------------------------------------------------------------------------- /phpcs-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DrupalDriver coding standard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | src/Drupal/Driver/Cores/*.php 18 | 19 | 20 | */doc/* 21 | */drush/* 22 | */reports/* 23 | */spec/* 24 | */vendor/* 25 | */CHANGELOG.md 26 | */README.md 27 | ./drupal/* 28 | 29 | 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src/Drupal/ 6 | 7 | 8 | 9 | 10 | ./tests/Drupal/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/BlackboxDriverSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Drupal\Driver\BlackboxDriver'); 13 | } 14 | 15 | function it_is_always_bootstrapped() 16 | { 17 | $this->isBootStrapped()->shouldReturn(TRUE); 18 | } 19 | 20 | function it_should_not_allow_api_methods() 21 | { 22 | $user = $node = $term = new \stdClass(); 23 | $this->shouldThrow('Drupal\Driver\Exception\UnsupportedDriverActionException')->duringUserCreate($user); 24 | $this->shouldThrow('Drupal\Driver\Exception\UnsupportedDriverActionException')->duringCreateNode($node); 25 | $this->shouldThrow('Drupal\Driver\Exception\UnsupportedDriverActionException')->duringCreateTerm($term); 26 | } 27 | 28 | function it_should_not_have_a_random_generator() 29 | { 30 | $this->shouldThrow('Drupal\Driver\Exception\UnsupportedDriverActionException')->duringGetRandom(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/Cores/Drupal6Spec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('path', 'http://www.example.com', $random); 15 | } 16 | 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Drupal\Driver\Cores\Drupal6'); 20 | } 21 | 22 | function it_should_return_a_random_generator() 23 | { 24 | $this->getRandom()->shouldBeAnInstanceOf('Drupal\Component\Utility\Random'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/Cores/Drupal7Spec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('path', 'http://www.example.com', $random); 15 | } 16 | 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Drupal\Driver\Cores\Drupal7'); 20 | } 21 | 22 | function it_should_return_a_random_generator() 23 | { 24 | $this->getRandom()->shouldBeAnInstanceOf('Drupal\Component\Utility\Random'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/Cores/Drupal8Spec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('path', 'http://www.example.com', $random); 16 | } 17 | 18 | function it_is_initializable() 19 | { 20 | $this->shouldHaveType('Drupal\Driver\Cores\Drupal8'); 21 | } 22 | 23 | function it_should_return_a_random_generator() 24 | { 25 | $this->getRandom()->shouldBeAnInstanceOf('Drupal\Component\Utility\Random'); 26 | } 27 | 28 | function it_is_an_auth_core() 29 | { 30 | $this->shouldBeAnInstanceOf(CoreAuthenticationInterface::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/Exception/BootstrapExceptionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('Failed to bootstrap!'); 13 | } 14 | 15 | function it_is_initializable() 16 | { 17 | $this->shouldHaveType('Drupal\Driver\Exception\BootstrapException'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/Drupal/Driver/Exception/UnsupportedDriverActionExceptionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('Unsupported action in %s driver!', $driver); 15 | } 16 | 17 | function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Drupal\Driver\Exception\UnsupportedDriverActionException'); 20 | } 21 | 22 | function it_should_get_the_driver() 23 | { 24 | $this->getDriver()->shouldBeAnInstanceOf('Drupal\Driver\DriverInterface'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Drupal/Driver/AuthenticationDriverInterface.php: -------------------------------------------------------------------------------- 1 | errorString('generate random'), $this); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function bootstrap() { 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function isBootstrapped() { 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function userCreate(\stdClass $user) { 35 | throw new UnsupportedDriverActionException($this->errorString('create users'), $this); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function userDelete(\stdClass $user) { 42 | throw new UnsupportedDriverActionException($this->errorString('delete users'), $this); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function processBatch() { 49 | throw new UnsupportedDriverActionException($this->errorString('process batch actions'), $this); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function userAddRole(\stdClass $user, $role) { 56 | throw new UnsupportedDriverActionException($this->errorString('add roles'), $this); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function fetchWatchdog($count = 10, $type = NULL, $severity = NULL) { 63 | throw new UnsupportedDriverActionException($this->errorString('access watchdog entries'), $this); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function clearCache($type = NULL) { 70 | throw new UnsupportedDriverActionException($this->errorString('clear Drupal caches'), $this); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function clearStaticCaches() { 77 | throw new UnsupportedDriverActionException($this->errorString('clear static caches'), $this); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function createNode($node) { 84 | throw new UnsupportedDriverActionException($this->errorString('create nodes'), $this); 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function nodeDelete($node) { 91 | throw new UnsupportedDriverActionException($this->errorString('delete nodes'), $this); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function runCron() { 98 | throw new UnsupportedDriverActionException($this->errorString('run cron'), $this); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function createTerm(\stdClass $term) { 105 | throw new UnsupportedDriverActionException($this->errorString('create terms'), $this); 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function termDelete(\stdClass $term) { 112 | throw new UnsupportedDriverActionException($this->errorString('delete terms'), $this); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function roleCreate(array $permissions) { 119 | throw new UnsupportedDriverActionException($this->errorString('create roles'), $this); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function roleDelete($rid) { 126 | throw new UnsupportedDriverActionException($this->errorString('delete roles'), $this); 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function isField($entity_type, $field_name) { 133 | return FALSE; 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function isBaseField($entity_type, $field_name) { 140 | return FALSE; 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | public function configGet($name, $key) { 147 | throw new UnsupportedDriverActionException($this->errorString('config get'), $this); 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function configSet($name, $key, $value) { 154 | throw new UnsupportedDriverActionException($this->errorString('config set'), $this); 155 | } 156 | 157 | /** 158 | * Error printing exception. 159 | * 160 | * @param string $error 161 | * The term, node, user or permission. 162 | * 163 | * @return string 164 | * A formatted string reminding people to use an API driver. 165 | */ 166 | private function errorString($error) { 167 | return sprintf('No ability to %s in %%s. Put `@api` into your feature and add an API driver (ex: `api_driver: drupal`) in behat.yml.', $error); 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function createEntity($entity_type, \stdClass $entity) { 174 | throw new UnsupportedDriverActionException($this->errorString('create entities using the generic Entity API'), $this); 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function entityDelete($entity_type, \stdClass $entity) { 181 | throw new UnsupportedDriverActionException($this->errorString('delete entities using the generic Entity API'), $this); 182 | } 183 | 184 | /** 185 | * {@inheritdoc} 186 | */ 187 | public function startCollectingMail() { 188 | throw new UnsupportedDriverActionException($this->errorString('work with mail'), $this); 189 | } 190 | 191 | /** 192 | * {@inheritdoc} 193 | */ 194 | public function stopCollectingMail() { 195 | throw new UnsupportedDriverActionException($this->errorString('work with mail'), $this); 196 | } 197 | 198 | /** 199 | * {@inheritdoc} 200 | */ 201 | public function getMail() { 202 | throw new UnsupportedDriverActionException($this->errorString('work with mail'), $this); 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function clearMail() { 209 | throw new UnsupportedDriverActionException($this->errorString('work with mail'), $this); 210 | } 211 | 212 | /** 213 | * {@inheritdoc} 214 | */ 215 | public function sendMail($body, $subject, $to, $langcode) { 216 | throw new UnsupportedDriverActionException($this->errorString('work with mail'), $this); 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/Drupal/Driver/BlackboxDriver.php: -------------------------------------------------------------------------------- 1 | drupalRoot = realpath($drupal_root); 39 | $this->uri = $uri; 40 | if (!isset($random)) { 41 | $random = new Random(); 42 | } 43 | $this->random = $random; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getRandom() { 50 | return $this->random; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getFieldHandler($entity, $entity_type, $field_name) { 57 | $reflection = new \ReflectionClass($this); 58 | $core_namespace = $reflection->getShortName(); 59 | $field_types = $this->getEntityFieldTypes($entity_type, [$field_name]); 60 | $camelized_type = Container::camelize($field_types[$field_name]); 61 | $default_class = sprintf('\Drupal\Driver\Fields\%s\DefaultHandler', $core_namespace); 62 | $class_name = sprintf('\Drupal\Driver\Fields\%s\%sHandler', $core_namespace, $camelized_type); 63 | if (class_exists($class_name)) { 64 | return new $class_name($entity, $entity_type, $field_name); 65 | } 66 | return new $default_class($entity, $entity_type, $field_name); 67 | } 68 | 69 | /** 70 | * Expands properties on the given entity object to the expected structure. 71 | * 72 | * @param string $entity_type 73 | * The entity type ID. 74 | * @param object $entity 75 | * Entity object. 76 | * @param array $base_fields 77 | * Optional. Define base fields that will be expanded in addition to user 78 | * defined fields. 79 | */ 80 | protected function expandEntityFields($entity_type, \stdClass $entity, array $base_fields = []) { 81 | $field_types = $this->getEntityFieldTypes($entity_type, $base_fields); 82 | foreach ($field_types as $field_name => $type) { 83 | if (isset($entity->$field_name)) { 84 | $entity->$field_name = $this->getFieldHandler($entity, $entity_type, $field_name) 85 | ->expand($entity->$field_name); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function clearStaticCaches() { 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Cores/CoreAuthenticationInterface.php: -------------------------------------------------------------------------------- 1 | drupalRoot); 26 | require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; 27 | $this->validateDrupalSite(); 28 | } 29 | 30 | // Bootstrap Drupal. 31 | $current_path = getcwd(); 32 | chdir(DRUPAL_ROOT); 33 | drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); 34 | if (empty($GLOBALS['db_url'])) { 35 | throw new BootstrapException('Missing database setting, verify the database configuration in settings.php.'); 36 | } 37 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 38 | chdir($current_path); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function clearCache() { 45 | // Need to change into the Drupal root directory or the registry explodes. 46 | $current_path = getcwd(); 47 | chdir(DRUPAL_ROOT); 48 | drupal_flush_all_caches(); 49 | chdir($current_path); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function nodeCreate($node) { 56 | $current_path = getcwd(); 57 | chdir(DRUPAL_ROOT); 58 | 59 | // Set original if not set. 60 | if (!isset($node->original)) { 61 | $node->original = clone $node; 62 | } 63 | 64 | // Assign authorship if none exists and `author` is passed. 65 | if (!isset($node->uid) && !empty($node->author) && ($user = user_load(['name' => $node->author]))) { 66 | $node->uid = $user->uid; 67 | } 68 | 69 | // Convert properties to expected structure. 70 | $this->expandEntityProperties($node); 71 | 72 | // Attempt to decipher any fields that may be specified. 73 | $this->expandEntityFields('node', $node); 74 | 75 | // Set defaults that haven't already been set. 76 | $defaults = clone $node; 77 | module_load_include('inc', 'node', 'node.pages'); 78 | node_object_prepare($defaults); 79 | $node = (object) array_merge((array) $defaults, (array) $node); 80 | 81 | node_save($node); 82 | 83 | chdir($current_path); 84 | return $node; 85 | 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function nodeDelete($node) { 92 | node_delete($node->nid); 93 | } 94 | 95 | /** 96 | * Implements CoreInterface::runCron(). 97 | */ 98 | public function runCron() { 99 | return drupal_cron_run(); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function userCreate(\stdClass $user) { 106 | // Default status to TRUE if not explicitly creating a blocked user. 107 | if (!isset($user->status)) { 108 | $user->status = 1; 109 | } 110 | 111 | // Clone user object, otherwise user_save() changes the password to the 112 | // hashed password. 113 | $account = clone $user; 114 | // Convert role array to a keyed array. 115 | if (isset($user->roles)) { 116 | $roles = []; 117 | foreach ($user->roles as $rid) { 118 | $roles[$rid] = $rid; 119 | } 120 | $user->roles = $roles; 121 | } 122 | $account = user_save((array) $account, (array) $account); 123 | // Store the UID. 124 | $user->uid = $account->uid; 125 | return $user; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function userDelete(\stdClass $user) { 132 | $current_path = getcwd(); 133 | chdir(DRUPAL_ROOT); 134 | user_delete((array) $user, $user->uid); 135 | chdir($current_path); 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function processBatch() { 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function userAddRole(\stdClass $user, $role_name) { 148 | $roles = array_flip(user_roles()); 149 | $role = $roles[$role_name]; 150 | if (!$role) { 151 | throw new \RuntimeException(sprintf('No role "%s" exists.', $role_name)); 152 | } 153 | user_multiple_role_edit([$user->uid], 'add_role', $role); 154 | } 155 | 156 | /** 157 | * Fetches a user role by role name. 158 | * 159 | * @param string $role_name 160 | * A string representing the role name. 161 | * 162 | * @return object 163 | * A fully-loaded role object if a role with the given name exists, or FALSE 164 | * otherwise. 165 | * 166 | * @see user_role_load() 167 | */ 168 | protected function userRoleLoadByName($role_name) { 169 | $result = db_query('SELECT * FROM {role} WHERE name = "%s"', $role_name); 170 | return db_fetch_object($result); 171 | } 172 | 173 | /** 174 | * Check to make sure that the array of permissions are valid. 175 | * 176 | * @param array $permissions 177 | * Permissions to check. 178 | * @param bool $reset 179 | * Reset cached available permissions. 180 | * 181 | * @return bool 182 | * TRUE or FALSE depending on whether the permissions are valid. 183 | */ 184 | protected function checkPermissions(array $permissions, $reset = FALSE) { 185 | 186 | if (!isset($this->availablePermissons) || $reset) { 187 | $this->availablePermissons = array_keys(module_invoke_all('permission')); 188 | } 189 | 190 | $valid = TRUE; 191 | foreach ($permissions as $permission) { 192 | if (!in_array($permission, $this->availablePermissons)) { 193 | $valid = FALSE; 194 | } 195 | } 196 | return $valid; 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function roleCreate(array $permissions) { 203 | // Verify permissions exist. 204 | $all_permissions = module_invoke_all('perm'); 205 | foreach ($permissions as $name) { 206 | $search = array_search($name, $all_permissions); 207 | if (!$search) { 208 | throw new \RuntimeException(sprintf("No permission '%s' exists.", $name)); 209 | } 210 | } 211 | // Create new role. 212 | $name = $this->random->name(8); 213 | db_query("INSERT INTO {role} SET name = '%s'", $name); 214 | // Add permissions to role. 215 | $rid = db_last_insert_id('role', 'rid'); 216 | db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')", $rid, implode(', ', $permissions)); 217 | return $name; 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function roleDelete($role_name) { 224 | $roles = array_flip(user_roles()); 225 | $rid = $roles[$role_name]; 226 | db_query('DELETE FROM {role} WHERE rid = %d', $rid); 227 | if (!db_affected_rows()) { 228 | throw new \RuntimeException(sprintf('No role "%s" exists.', $rid)); 229 | } 230 | } 231 | 232 | /** 233 | * {@inheritdoc} 234 | */ 235 | public function validateDrupalSite() { 236 | if ('default' !== $this->uri) { 237 | // Fake the necessary HTTP headers that Drupal needs: 238 | $drupal_base_url = parse_url($this->uri); 239 | // If there's no url scheme set, add http:// and re-parse the url 240 | // so the host and path values are set accurately. 241 | if (!array_key_exists('scheme', $drupal_base_url)) { 242 | $drupal_base_url = parse_url($this->uri); 243 | } 244 | // Fill in defaults. 245 | $drupal_base_url += [ 246 | 'path' => NULL, 247 | 'host' => NULL, 248 | 'port' => NULL, 249 | ]; 250 | $_SERVER['HTTP_HOST'] = $drupal_base_url['host']; 251 | 252 | if ($drupal_base_url['port']) { 253 | $_SERVER['HTTP_HOST'] .= ':' . $drupal_base_url['port']; 254 | } 255 | $_SERVER['SERVER_PORT'] = $drupal_base_url['port']; 256 | 257 | if (array_key_exists('path', $drupal_base_url)) { 258 | $_SERVER['PHP_SELF'] = $drupal_base_url['path'] . '/index.php'; 259 | } 260 | else { 261 | $_SERVER['PHP_SELF'] = '/index.php'; 262 | } 263 | } 264 | else { 265 | $_SERVER['HTTP_HOST'] = 'default'; 266 | $_SERVER['PHP_SELF'] = '/index.php'; 267 | } 268 | 269 | $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; 270 | $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 271 | $_SERVER['REQUEST_METHOD'] = NULL; 272 | 273 | $_SERVER['SERVER_SOFTWARE'] = NULL; 274 | $_SERVER['HTTP_USER_AGENT'] = NULL; 275 | 276 | $conf_path = conf_path(TRUE, TRUE); 277 | $conf_file = $this->drupalRoot . "/$conf_path/settings.php"; 278 | if (!file_exists($conf_file)) { 279 | throw new BootstrapException(sprintf('Could not find a Drupal settings.php file at "%s"', $conf_file)); 280 | } 281 | $drushrc_file = $this->drupalRoot . "/$conf_path/drushrc.php"; 282 | if (file_exists($drushrc_file)) { 283 | require_once $drushrc_file; 284 | } 285 | } 286 | 287 | /** 288 | * Expands properties on the given entity object to the expected structure. 289 | * 290 | * @param object $entity 291 | * The entity object. 292 | */ 293 | protected function expandEntityProperties(\stdClass $entity) { 294 | // The created field may come in as a readable date, rather than a 295 | // timestamp. 296 | if (isset($entity->created) && !is_numeric($entity->created)) { 297 | $entity->created = strtotime($entity->created); 298 | } 299 | 300 | // Map human-readable node types to machine node types. 301 | $types = node_get_types(); 302 | foreach ($types as $type) { 303 | if ($entity->type == $type->name) { 304 | $entity->type = $type->type; 305 | continue; 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * Load vocabularies, optional by VIDs. 312 | * 313 | * @param array $vids 314 | * The vids to load. 315 | * 316 | * @return array 317 | * An array of vocabulary objects 318 | */ 319 | protected function taxonomyVocabularyLoadMultiple(array $vids = []) { 320 | $vocabularies = taxonomy_get_vocabularies(); 321 | if ($vids) { 322 | return array_intersect_key($vocabularies, array_flip($vids)); 323 | } 324 | return $vocabularies; 325 | } 326 | 327 | /** 328 | * {@inheritdoc} 329 | */ 330 | public function termCreate(\stdClass $term) { 331 | // Map vocabulary names to vid, these take precedence over machine names. 332 | if (!isset($term->vid)) { 333 | $vocabularies = \taxonomy_get_vocabularies(); 334 | foreach ($vocabularies as $vid => $vocabulary) { 335 | if ($vocabulary->name == $term->vocabulary_machine_name) { 336 | $term->vid = $vocabulary->vid; 337 | } 338 | } 339 | } 340 | 341 | if (!isset($term->vid)) { 342 | 343 | // Try to load vocabulary by machine name. 344 | $vocabularies = $this->taxonomyVocabularyLoadMultiple([$term->vid]); 345 | if (!empty($vocabularies)) { 346 | $vids = array_keys($vocabularies); 347 | $term->vid = reset($vids); 348 | } 349 | } 350 | 351 | // If `parent` is set, look up a term in this vocab with that name. 352 | if (isset($term->parent)) { 353 | $parent = \taxonomy_get_term_by_name($term->parent); 354 | if (!empty($parent)) { 355 | $parent = reset($parent); 356 | $term->parent = $parent->tid; 357 | } 358 | } 359 | 360 | if (empty($term->vid)) { 361 | throw new \Exception(sprintf('No "%s" vocabulary found.')); 362 | } 363 | 364 | // Attempt to decipher any fields that may be specified. 365 | $this->expandEntityFields('taxonomy_term', $term); 366 | 367 | // Protect against a failure from hook_taxonomy_term_insert() in pathauto. 368 | $current_path = getcwd(); 369 | chdir(DRUPAL_ROOT); 370 | $term_array = (array) $term; 371 | \taxonomy_save_term($term_array); 372 | chdir($current_path); 373 | 374 | // Loading a term by name returns an array of term objects, but there should 375 | // only be one matching term in a testing context, so take the first match 376 | // by reset()'ing $matches. 377 | $matches = \taxonomy_get_term_by_name($term->name); 378 | $saved_term = reset($matches); 379 | 380 | return $saved_term; 381 | } 382 | 383 | /** 384 | * {@inheritdoc} 385 | */ 386 | public function termDelete(\stdClass $term) { 387 | $status = 0; 388 | if (isset($term->tid)) { 389 | $status = \taxonomy_del_term($term->tid); 390 | } 391 | // Will be SAVED_DELETED (3) on success. 392 | return $status; 393 | } 394 | 395 | /** 396 | * Helper function to get all permissions. 397 | * 398 | * @return array 399 | * Array keyed by permission name, with the human-readable title as the 400 | * value. 401 | */ 402 | protected function getAllPermissions() { 403 | $permissions = []; 404 | foreach (module_invoke_all('permission') as $name => $permission) { 405 | $permissions[$name] = $permission['title']; 406 | } 407 | return $permissions; 408 | } 409 | 410 | /** 411 | * {@inheritdoc} 412 | */ 413 | public function getModuleList() { 414 | return module_list(); 415 | } 416 | 417 | /** 418 | * {@inheritdoc} 419 | */ 420 | public function getExtensionPathList() { 421 | $paths = []; 422 | 423 | // Get enabled modules. 424 | $modules = $this->getModuleList(); 425 | foreach ($modules as $module) { 426 | $paths[] = $this->drupalRoot . DIRECTORY_SEPARATOR . \drupal_get_path('module', $module); 427 | } 428 | 429 | return $paths; 430 | } 431 | 432 | /** 433 | * {@inheritdoc} 434 | */ 435 | protected function expandEntityFields($entity_type, \stdClass $entity, array $base_fields = []) { 436 | return parent::expandEntityFields($entity_type, $entity); 437 | } 438 | 439 | /** 440 | * {@inheritdoc} 441 | */ 442 | public function getEntityFieldTypes($entity_type, array $base_fields = []) { 443 | $taxonomy_fields = ['taxonomy' => 'taxonomy']; 444 | if (!module_exists('content')) { 445 | return $taxonomy_fields; 446 | } 447 | $return = []; 448 | $fields = content_fields(); 449 | foreach ($fields as $field_name => $field) { 450 | if ($this->isField($entity_type, $field_name)) { 451 | $return[$field_name] = $field['type']; 452 | } 453 | } 454 | 455 | $return += $taxonomy_fields; 456 | 457 | return $return; 458 | } 459 | 460 | /** 461 | * {@inheritdoc} 462 | */ 463 | public function isField($entity_type, $field_name) { 464 | if ($field_name === 'taxonomy') { 465 | return TRUE; 466 | } 467 | if (!module_exists('content')) { 468 | return FALSE; 469 | } 470 | $map = content_fields(); 471 | return isset($map[$field_name]); 472 | } 473 | 474 | /** 475 | * {@inheritdoc} 476 | */ 477 | public function isBaseField($entity_type, $field_name) { 478 | // Base fields are only supported in Drupal 8 and higher. 479 | return FALSE; 480 | } 481 | 482 | /** 483 | * {@inheritdoc} 484 | */ 485 | public function languageCreate(\stdClass $language) { 486 | throw new \Exception('Creating languages is not yet implemented for Drupal 6.'); 487 | } 488 | 489 | /** 490 | * {@inheritdoc} 491 | */ 492 | public function languageDelete(\stdClass $language) { 493 | throw new \Exception('Deleting languages is not yet implemented for Drupal 6.'); 494 | } 495 | 496 | /** 497 | * {@inheritdoc} 498 | */ 499 | public function configGet($name, $key = '') { 500 | throw new \Exception('Getting config is not yet implemented for Drupal 6.'); 501 | } 502 | 503 | /** 504 | * {@inheritdoc} 505 | */ 506 | public function configGetOriginal($name, $key = '') { 507 | throw new \Exception('Getting original config is not yet implemented for Drupal 6.'); 508 | } 509 | 510 | /** 511 | * {@inheritdoc} 512 | */ 513 | public function configSet($name, $key, $value) { 514 | throw new \Exception('Setting config is not yet implemented for Drupal 6.'); 515 | } 516 | 517 | /** 518 | * {@inheritdoc} 519 | */ 520 | public function clearStaticCaches() { 521 | // Drupal 6 doesn't have a way of clearing all static caches. 522 | } 523 | 524 | /** 525 | * {@inheritdoc} 526 | */ 527 | public function entityCreate($entity_type, $entity) { 528 | throw new \Exception('Drupal 6 does not have a generic Entity API, so creation of entities is not possible in this way.'); 529 | } 530 | 531 | /** 532 | * {@inheritdoc} 533 | */ 534 | public function entityDelete($entity_type, $entity) { 535 | throw new \Exception('Drupal 6 does not have a generic Entity API, so deletion of entities is not possible in this way.'); 536 | } 537 | 538 | /** 539 | * {@inheritdoc} 540 | */ 541 | public function startCollectingMail() { 542 | // @todo create a D6 version of this function 543 | throw new \Exception('Mail testing is not yet implemented for Drupal 6.'); 544 | } 545 | 546 | /** 547 | * {@inheritdoc} 548 | */ 549 | public function stopCollectingMail() { 550 | // @todo create a D6 version of this function 551 | throw new \Exception('Mail testing is not yet implemented for Drupal 6.'); 552 | } 553 | 554 | /** 555 | * {@inheritdoc} 556 | */ 557 | public function getMail() { 558 | // @todo create a D6 version of this function 559 | throw new \Exception('Mail testing is not yet implemented for Drupal 6.'); 560 | } 561 | 562 | /** 563 | * {@inheritdoc} 564 | */ 565 | public function clearMail() { 566 | // @todo create a D6 version of this function 567 | throw new \Exception('Mail testing is not yet implemented for Drupal 6.'); 568 | } 569 | 570 | /** 571 | * {@inheritdoc} 572 | */ 573 | public function sendMail($body, $subject = '', $to = '', $langcode = '') { 574 | // @todo create a D6 version of this function 575 | throw new \Exception('Mail testing is not yet implemented for Drupal 6.'); 576 | } 577 | 578 | } 579 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Cores/Drupal7.php: -------------------------------------------------------------------------------- 1 | drupalRoot); 19 | require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; 20 | $this->validateDrupalSite(); 21 | } 22 | 23 | // Bootstrap Drupal. 24 | chdir(DRUPAL_ROOT); 25 | drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); 26 | if (empty($GLOBALS['databases'])) { 27 | throw new BootstrapException('Missing database setting, verify the database configuration in settings.php.'); 28 | } 29 | drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function clearCache() { 36 | drupal_flush_all_caches(); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function nodeCreate($node) { 43 | // Assign authorship if none exists and `author` is passed. 44 | if (!isset($node->uid) && !empty($node->author) && ($user = user_load_by_name($node->author))) { 45 | $node->uid = $user->uid; 46 | } 47 | 48 | // Convert properties to expected structure. 49 | $this->expandEntityProperties($node); 50 | 51 | // Attempt to decipher any fields that may be specified. 52 | $this->expandEntityFields('node', $node); 53 | 54 | // Set defaults that haven't already been set. 55 | $defaults = clone $node; 56 | node_object_prepare($defaults); 57 | $node = (object) array_merge((array) $defaults, (array) $node); 58 | 59 | node_save($node); 60 | return $node; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function nodeDelete($node) { 67 | node_delete($node->nid); 68 | } 69 | 70 | /** 71 | * Implements CoreInterface::runCron(). 72 | */ 73 | public function runCron() { 74 | return drupal_cron_run(); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function userCreate(\stdClass $user) { 81 | // Default status to TRUE if not explicitly creating a blocked user. 82 | if (!isset($user->status)) { 83 | $user->status = 1; 84 | } 85 | 86 | // Clone user object, otherwise user_save() changes the password to the 87 | // hashed password. 88 | $account = clone $user; 89 | 90 | // Attempt to decipher any fields that may be specified. 91 | $this->expandEntityFields('user', $account); 92 | 93 | user_save($account, (array) $account); 94 | 95 | // Store UID. 96 | $user->uid = $account->uid; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function userDelete(\stdClass $user) { 103 | user_cancel([], $user->uid, 'user_cancel_delete'); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function processBatch() { 110 | $batch =& batch_get(); 111 | $batch['progressive'] = FALSE; 112 | batch_process(); 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function userAddRole(\stdClass $user, $role_name) { 119 | $role = user_role_load_by_name($role_name); 120 | 121 | if (!$role) { 122 | throw new \RuntimeException(sprintf('No role "%s" exists.', $role_name)); 123 | } 124 | 125 | user_multiple_role_edit([$user->uid], 'add_role', $role->rid); 126 | $account = user_load($user->uid); 127 | $user->roles = $account->roles; 128 | 129 | } 130 | 131 | /** 132 | * Check to make sure that the array of permissions are valid. 133 | * 134 | * @param array $permissions 135 | * Permissions to check. 136 | * @param bool $reset 137 | * Reset cached available permissions. 138 | * 139 | * @return bool 140 | * TRUE or FALSE depending on whether the permissions are valid. 141 | */ 142 | protected function checkPermissions(array $permissions, $reset = FALSE) { 143 | $available = &drupal_static(__FUNCTION__); 144 | 145 | if (!isset($available) || $reset) { 146 | $available = array_keys(module_invoke_all('permission')); 147 | } 148 | 149 | $valid = TRUE; 150 | foreach ($permissions as $permission) { 151 | if (!in_array($permission, $available)) { 152 | $valid = FALSE; 153 | } 154 | } 155 | return $valid; 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function roleCreate(array $permissions) { 162 | 163 | // Both machine name and permission title are allowed. 164 | $all_permissions = $this->getAllPermissions(); 165 | 166 | foreach ($permissions as $key => $name) { 167 | if (!isset($all_permissions[$name])) { 168 | $search = array_search($name, $all_permissions); 169 | if (!$search) { 170 | throw new \RuntimeException(sprintf("No permission '%s' exists.", $name)); 171 | } 172 | $permissions[$key] = $search; 173 | } 174 | } 175 | 176 | // Create new role. 177 | $role = new \stdClass(); 178 | $role->name = $this->random->name(8); 179 | user_role_save($role); 180 | user_role_grant_permissions($role->rid, $permissions); 181 | 182 | if ($role && !empty($role->rid)) { 183 | return $role->name; 184 | } 185 | 186 | throw new \RuntimeException(sprintf('Failed to create a role with "" permission(s).', implode(', ', $permissions))); 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function roleDelete($role_name) { 193 | $role = user_role_load_by_name($role_name); 194 | user_role_delete((int) $role->rid); 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function validateDrupalSite() { 201 | if ('default' !== $this->uri) { 202 | // Fake the necessary HTTP headers that Drupal needs: 203 | $drupal_base_url = parse_url($this->uri); 204 | // If there's no url scheme set, add http:// and re-parse the url 205 | // so the host and path values are set accurately. 206 | if (!array_key_exists('scheme', $drupal_base_url)) { 207 | $drupal_base_url = parse_url($this->uri); 208 | } 209 | // Fill in defaults. 210 | $drupal_base_url += [ 211 | 'path' => NULL, 212 | 'host' => NULL, 213 | 'port' => NULL, 214 | ]; 215 | $_SERVER['HTTP_HOST'] = $drupal_base_url['host']; 216 | 217 | if ($drupal_base_url['port']) { 218 | $_SERVER['HTTP_HOST'] .= ':' . $drupal_base_url['port']; 219 | } 220 | $_SERVER['SERVER_PORT'] = $drupal_base_url['port']; 221 | 222 | if (array_key_exists('path', $drupal_base_url)) { 223 | $_SERVER['PHP_SELF'] = $drupal_base_url['path'] . '/index.php'; 224 | } 225 | else { 226 | $_SERVER['PHP_SELF'] = '/index.php'; 227 | } 228 | } 229 | else { 230 | $_SERVER['HTTP_HOST'] = 'default'; 231 | $_SERVER['PHP_SELF'] = '/index.php'; 232 | } 233 | 234 | $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; 235 | $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 236 | $_SERVER['REQUEST_METHOD'] = NULL; 237 | 238 | $_SERVER['SERVER_SOFTWARE'] = NULL; 239 | $_SERVER['HTTP_USER_AGENT'] = NULL; 240 | 241 | $conf_path = conf_path(TRUE, TRUE); 242 | $conf_file = $this->drupalRoot . "/$conf_path/settings.php"; 243 | if (!file_exists($conf_file)) { 244 | throw new BootstrapException(sprintf('Could not find a Drupal settings.php file at "%s"', $conf_file)); 245 | } 246 | $drushrc_file = $this->drupalRoot . "/$conf_path/drushrc.php"; 247 | if (file_exists($drushrc_file)) { 248 | require_once $drushrc_file; 249 | } 250 | } 251 | 252 | /** 253 | * Expands properties on the given entity object to the expected structure. 254 | * 255 | * @param object $entity 256 | * The entity object. 257 | */ 258 | protected function expandEntityProperties(\stdClass $entity) { 259 | // The created field may come in as a readable date, rather than a 260 | // timestamp. 261 | if (isset($entity->created) && !is_numeric($entity->created)) { 262 | $entity->created = strtotime($entity->created); 263 | } 264 | 265 | // Map human-readable node types to machine node types. 266 | $types = \node_type_get_types(); 267 | foreach ($types as $type) { 268 | if ($entity->type == $type->name) { 269 | $entity->type = $type->type; 270 | continue; 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * {@inheritdoc} 277 | */ 278 | public function termCreate(\stdClass $term) { 279 | // Map vocabulary names to vid, these take precedence over machine names. 280 | if (!isset($term->vid)) { 281 | $vocabularies = \taxonomy_get_vocabularies(); 282 | foreach ($vocabularies as $vid => $vocabulary) { 283 | if ($vocabulary->name == $term->vocabulary_machine_name) { 284 | $term->vid = $vocabulary->vid; 285 | } 286 | } 287 | } 288 | 289 | if (!isset($term->vid)) { 290 | 291 | // Try to load vocabulary by machine name. 292 | $vocabularies = \taxonomy_vocabulary_load_multiple(FALSE, [ 293 | 'machine_name' => $term->vocabulary_machine_name, 294 | ]); 295 | if (!empty($vocabularies)) { 296 | $vids = array_keys($vocabularies); 297 | $term->vid = reset($vids); 298 | } 299 | } 300 | 301 | // If `parent` is set, look up a term in this vocab with that name. 302 | if (isset($term->parent)) { 303 | $parent = \taxonomy_get_term_by_name($term->parent, $term->vocabulary_machine_name); 304 | if (!empty($parent)) { 305 | $parent = reset($parent); 306 | $term->parent = $parent->tid; 307 | } 308 | } 309 | 310 | if (empty($term->vid)) { 311 | throw new \Exception(sprintf('No "%s" vocabulary found.', $term->vocabulary_machine_name)); 312 | } 313 | 314 | // Attempt to decipher any fields that may be specified. 315 | $this->expandEntityFields('taxonomy_term', $term); 316 | 317 | \taxonomy_term_save($term); 318 | 319 | return $term; 320 | } 321 | 322 | /** 323 | * {@inheritdoc} 324 | */ 325 | public function termDelete(\stdClass $term) { 326 | $status = 0; 327 | if (isset($term->tid)) { 328 | $status = \taxonomy_term_delete($term->tid); 329 | } 330 | // Will be SAVED_DELETED (3) on success. 331 | return $status; 332 | } 333 | 334 | /** 335 | * {@inheritdoc} 336 | */ 337 | public function languageCreate(\stdClass $language) { 338 | if (!module_exists('locale')) { 339 | throw new \Exception(sprintf("%s::%s line %s: This driver requires the 'locale' module be enabled in order to create languages", get_class($this), __FUNCTION__, __LINE__)); 340 | } 341 | include_once DRUPAL_ROOT . '/includes/iso.inc'; 342 | include_once DRUPAL_ROOT . '/includes/locale.inc'; 343 | 344 | // Get all predefined languages, regardless if they are enabled or not. 345 | $predefined_languages = _locale_get_predefined_list(); 346 | 347 | // If the language code is not valid then throw an InvalidArgumentException. 348 | if (!isset($predefined_languages[$language->langcode])) { 349 | throw new \InvalidArgumentException("There is no predefined language with langcode '{$language->langcode}'."); 350 | } 351 | 352 | // Enable a language only if it has not been enabled already. 353 | $enabled_languages = locale_language_list(); 354 | if (!isset($enabled_languages[$language->langcode])) { 355 | locale_add_language($language->langcode); 356 | return $language; 357 | } 358 | 359 | return FALSE; 360 | } 361 | 362 | /** 363 | * {@inheritdoc} 364 | */ 365 | public function languageDelete(\stdClass $language) { 366 | $langcode = $language->langcode; 367 | // Do not remove English or the default language. 368 | if (!in_array($langcode, [language_default('language'), 'en'])) { 369 | // @see locale_languages_delete_form_submit(). 370 | $languages = language_list(); 371 | if (isset($languages[$langcode])) { 372 | // Remove translations first. 373 | db_delete('locales_target') 374 | ->condition('language', $langcode) 375 | ->execute(); 376 | cache_clear_all('locale:' . $langcode, 'cache'); 377 | // With no translations, this removes existing JavaScript translations 378 | // file. 379 | _locale_rebuild_js($langcode); 380 | // Remove the language. 381 | db_delete('languages') 382 | ->condition('language', $langcode) 383 | ->execute(); 384 | db_update('node') 385 | ->fields(['language' => '']) 386 | ->condition('language', $langcode) 387 | ->execute(); 388 | if ($languages[$langcode]->enabled) { 389 | variable_set('language_count', variable_get('language_count', 1) - 1); 390 | } 391 | module_invoke_all('multilingual_settings_changed'); 392 | drupal_static_reset('language_list'); 393 | } 394 | 395 | // Changing the language settings impacts the interface: 396 | cache_clear_all('*', 'cache_page', TRUE); 397 | } 398 | } 399 | 400 | /** 401 | * {@inheritdoc} 402 | */ 403 | public function configGet($name, $key = '') { 404 | throw new \Exception('Getting config is not yet implemented for Drupal 7.'); 405 | } 406 | 407 | /** 408 | * {@inheritdoc} 409 | */ 410 | public function configGetOriginal($name, $key = '') { 411 | throw new \Exception('Getting original config is not yet implemented for Drupal 7.'); 412 | } 413 | 414 | /** 415 | * {@inheritdoc} 416 | */ 417 | public function configSet($name, $key, $value) { 418 | throw new \Exception('Setting config is not yet implemented for Drupal 7.'); 419 | } 420 | 421 | /** 422 | * Helper function to get all permissions. 423 | * 424 | * @return array 425 | * Array keyed by permission name, with the human-readable title as the 426 | * value. 427 | */ 428 | protected function getAllPermissions() { 429 | $permissions = []; 430 | foreach (module_invoke_all('permission') as $name => $permission) { 431 | $permissions[$name] = $permission['title']; 432 | } 433 | return $permissions; 434 | } 435 | 436 | /** 437 | * {@inheritdoc} 438 | */ 439 | public function getModuleList() { 440 | return module_list(); 441 | } 442 | 443 | /** 444 | * {@inheritdoc} 445 | */ 446 | public function getExtensionPathList() { 447 | $paths = []; 448 | 449 | // Get enabled modules. 450 | $modules = $this->getModuleList(); 451 | foreach ($modules as $module) { 452 | $paths[] = $this->drupalRoot . DIRECTORY_SEPARATOR . \drupal_get_path('module', $module); 453 | } 454 | 455 | return $paths; 456 | } 457 | 458 | /** 459 | * {@inheritdoc} 460 | */ 461 | public function getEntityFieldTypes($entity_type, array $base_fields = []) { 462 | $return = []; 463 | $fields = field_info_field_map(); 464 | foreach ($fields as $field_name => $field) { 465 | if (array_key_exists($entity_type, $field['bundles'])) { 466 | $return[$field_name] = $field['type']; 467 | } 468 | } 469 | return $return; 470 | } 471 | 472 | /** 473 | * {@inheritdoc} 474 | */ 475 | public function isField($entity_type, $field_name) { 476 | $map = field_info_field_map(); 477 | return !empty($map[$field_name]) && array_key_exists($entity_type, $map[$field_name]['bundles']); 478 | } 479 | 480 | /** 481 | * {@inheritdoc} 482 | */ 483 | public function isBaseField($entity_type, $field_name) { 484 | // Base fields are only supported in Drupal 8 and higher. 485 | return FALSE; 486 | } 487 | 488 | /** 489 | * {@inheritdoc} 490 | */ 491 | public function clearStaticCaches() { 492 | drupal_static_reset(); 493 | } 494 | 495 | /** 496 | * {@inheritdoc} 497 | */ 498 | public function entityCreate($entity_type, $entity) { 499 | $info = entity_get_info($entity_type); 500 | // If the bundle field is empty, put the inferred bundle value in it. 501 | $bundle_key = $info['entity keys']['bundle']; 502 | if (!isset($entity->$bundle_key) && isset($entity->step_bundle)) { 503 | $entity->$bundle_key = $entity->step_bundle; 504 | } 505 | 506 | // Throw an exception if a bundle is specified but does not exist. 507 | if (isset($entity->$bundle_key) && ($entity->$bundle_key !== NULL)) { 508 | $bundles = $info['bundles']; 509 | if (!in_array($entity->$bundle_key, array_keys($bundles))) { 510 | throw new \Exception("Cannot create entity because provided bundle {$entity->$bundle_key} does not exist."); 511 | } 512 | } 513 | if (empty($entity_type)) { 514 | throw new \Exception("You must specify an entity type to create an entity."); 515 | } 516 | 517 | $this->expandEntityFields($entity_type, $entity); 518 | $createdEntity = entity_create($entity_type, (array) $entity); 519 | 520 | // In D7 it's possible that $createdEntity is not of class Entity, so we 521 | // must use entity_save(). 522 | entity_save($entity_type, $createdEntity); 523 | 524 | list($id) = entity_extract_ids($entity_type, $createdEntity); 525 | $createdEntity->id = $id; 526 | 527 | return $createdEntity; 528 | } 529 | 530 | /** 531 | * {@inheritdoc} 532 | */ 533 | public function entityDelete($entity_type, $entity) { 534 | // In D7 it's possible that $entity is not of class Entity, so we must use 535 | // entity_delete(). 536 | list($id) = entity_extract_ids($entity_type, $entity); 537 | entity_delete($entity_type, $id); 538 | } 539 | 540 | /** 541 | * {@inheritdoc} 542 | */ 543 | public function startCollectingMail() { 544 | // @todo create a D7 version of this function 545 | throw new \Exception('Mail testing is not yet implemented for Drupal 7.'); 546 | } 547 | 548 | /** 549 | * {@inheritdoc} 550 | */ 551 | public function stopCollectingMail() { 552 | // @todo create a D7 version of this function 553 | throw new \Exception('Mail testing is not yet implemented for Drupal 7.'); 554 | } 555 | 556 | /** 557 | * {@inheritdoc} 558 | */ 559 | public function getMail() { 560 | // @todo create a D7 version of this function 561 | throw new \Exception('Mail testing is not yet implemented for Drupal 7.'); 562 | } 563 | 564 | /** 565 | * {@inheritdoc} 566 | */ 567 | public function clearMail() { 568 | // @todo create a D7 version of this function 569 | throw new \Exception('Mail testing is not yet implemented for Drupal 7.'); 570 | } 571 | 572 | /** 573 | * {@inheritdoc} 574 | */ 575 | public function sendMail($body, $subject = '', $to = '', $langcode = '') { 576 | // @todo create a D7 version of this function 577 | throw new \Exception('Mail testing is not yet implemented for Drupal 7.'); 578 | } 579 | 580 | } 581 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Cores/Drupal8.php: -------------------------------------------------------------------------------- 1 | drupalRoot); 45 | } 46 | 47 | // Bootstrap Drupal. 48 | chdir(DRUPAL_ROOT); 49 | $autoloader = require DRUPAL_ROOT . '/autoload.php'; 50 | require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; 51 | $this->validateDrupalSite(); 52 | 53 | $request = Request::createFromGlobals(); 54 | $kernel = DrupalKernel::createFromRequest($request, $autoloader, 'prod'); 55 | $kernel->boot(); 56 | // A route is required for route matching. In order to support Drupal 10 57 | // along with 8/9, we use the hardcoded values of RouteObjectInterface 58 | // constants ROUTE_NAME and ROUTE_OBJECT. 59 | // @see https://www.drupal.org/node/3151009 60 | $request->attributes->set('_route_object', new Route('')); 61 | $request->attributes->set('_route', ''); 62 | $kernel->preHandle($request); 63 | 64 | // Initialise an anonymous session. required for the bootstrap. 65 | \Drupal::service('session_manager')->start(); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function clearCache() { 72 | // Need to change into the Drupal root directory or the registry explodes. 73 | drupal_flush_all_caches(); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function nodeCreate($node) { 80 | // Throw an exception if the node type is missing or does not exist. 81 | /** @var \Drupal\node\Entity\Node $node */ 82 | if (!isset($node->type) || !$node->type) { 83 | throw new \Exception("Cannot create content because it is missing the required property 'type'."); 84 | } 85 | 86 | /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */ 87 | $bundle_info = \Drupal::service('entity_type.bundle.info'); 88 | $bundles = $bundle_info->getBundleInfo('node'); 89 | if (!in_array($node->type, array_keys($bundles))) { 90 | throw new \Exception(sprintf('Cannot create content because provided content type %s does not exist.', $node->type)); 91 | } 92 | // If 'author' is set, remap it to 'uid'. 93 | if (isset($node->author)) { 94 | $user = user_load_by_name($node->author); 95 | /** @var \Drupal\user\Entity\User $user */ 96 | if ($user) { 97 | $node->uid = $user->id(); 98 | } 99 | } 100 | $this->expandEntityFields('node', $node); 101 | $entity = Node::create((array) $node); 102 | $entity->save(); 103 | 104 | $node->nid = $entity->id(); 105 | 106 | return $node; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function nodeDelete($node) { 113 | $node = $node instanceof NodeInterface ? $node : Node::load($node->nid); 114 | if ($node instanceof NodeInterface) { 115 | $node->delete(); 116 | } 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | public function runCron() { 123 | return \Drupal::service('cron')->run(); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function userCreate(\stdClass $user) { 130 | $this->validateDrupalSite(); 131 | 132 | // Default status to TRUE if not explicitly creating a blocked user. 133 | if (!isset($user->status)) { 134 | $user->status = 1; 135 | } 136 | 137 | // Clone user object, otherwise user_save() changes the password to the 138 | // hashed password. 139 | $this->expandEntityFields('user', $user); 140 | $account = \Drupal::entityTypeManager()->getStorage('user')->create((array) $user); 141 | $account->save(); 142 | 143 | // Store UID. 144 | $user->uid = $account->id(); 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function roleCreate(array $permissions) { 151 | // Generate a random, lowercase machine name. 152 | $rid = strtolower($this->random->name(8, TRUE)); 153 | 154 | // Generate a random label. 155 | $name = trim($this->random->name(8, TRUE)); 156 | 157 | // Convert labels to machine names. 158 | $this->convertPermissions($permissions); 159 | 160 | // Check the all the permissions strings are valid. 161 | $this->checkPermissions($permissions); 162 | 163 | // Create new role. 164 | $role = \Drupal::entityTypeManager()->getStorage('user_role')->create([ 165 | 'id' => $rid, 166 | 'label' => $name, 167 | ]); 168 | $result = $role->save(); 169 | 170 | if ($result === SAVED_NEW) { 171 | // Grant the specified permissions to the role, if any. 172 | if (!empty($permissions)) { 173 | user_role_grant_permissions($role->id(), $permissions); 174 | } 175 | return $role->id(); 176 | } 177 | 178 | throw new \RuntimeException(sprintf('Failed to create a role with "%s" permission(s).', implode(', ', $permissions))); 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function roleDelete($role_name) { 185 | $role = Role::load($role_name); 186 | 187 | if (!$role) { 188 | throw new \RuntimeException(sprintf('No role "%s" exists.', $role_name)); 189 | } 190 | 191 | $role->delete(); 192 | } 193 | 194 | /** 195 | * {@inheritdoc} 196 | */ 197 | public function processBatch() { 198 | $this->validateDrupalSite(); 199 | $batch =& batch_get(); 200 | $batch['progressive'] = FALSE; 201 | batch_process(); 202 | } 203 | 204 | /** 205 | * Retrieve all permissions. 206 | * 207 | * @return array 208 | * Array of all defined permissions. 209 | */ 210 | protected function getAllPermissions() { 211 | $permissions = &drupal_static(__FUNCTION__); 212 | 213 | if (!isset($permissions)) { 214 | $permissions = \Drupal::service('user.permissions')->getPermissions(); 215 | } 216 | 217 | return $permissions; 218 | } 219 | 220 | /** 221 | * Convert any permission labels to machine name. 222 | * 223 | * @param array &$permissions 224 | * Array of permission names. 225 | */ 226 | protected function convertPermissions(array &$permissions) { 227 | $all_permissions = $this->getAllPermissions(); 228 | 229 | foreach ($all_permissions as $name => $definition) { 230 | $key = array_search($definition['title'], $permissions); 231 | if (FALSE !== $key) { 232 | $permissions[$key] = $name; 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Check to make sure that the array of permissions are valid. 239 | * 240 | * @param array $permissions 241 | * Permissions to check. 242 | */ 243 | protected function checkPermissions(array &$permissions) { 244 | $available = array_keys($this->getAllPermissions()); 245 | 246 | foreach ($permissions as $permission) { 247 | if (!in_array($permission, $available)) { 248 | throw new \RuntimeException(sprintf('Invalid permission "%s".', $permission)); 249 | } 250 | } 251 | } 252 | 253 | /** 254 | * {@inheritdoc} 255 | */ 256 | public function userDelete(\stdClass $user) { 257 | user_cancel([], $user->uid, 'user_cancel_delete'); 258 | } 259 | 260 | /** 261 | * {@inheritdoc} 262 | */ 263 | public function userAddRole(\stdClass $user, $role_name) { 264 | // Allow both machine and human role names. 265 | $query = \Drupal::entityQuery('user_role'); 266 | $conditions = $query->orConditionGroup() 267 | ->condition('id', $role_name) 268 | ->condition('label', $role_name); 269 | $rids = $query 270 | ->condition($conditions) 271 | ->execute(); 272 | if (!$rids) { 273 | throw new \RuntimeException(sprintf('No role "%s" exists.', $role_name)); 274 | } 275 | 276 | $account = User::load($user->uid); 277 | $account->addRole(reset($rids)); 278 | $account->save(); 279 | } 280 | 281 | /** 282 | * {@inheritdoc} 283 | */ 284 | public function validateDrupalSite() { 285 | if ('default' !== $this->uri) { 286 | // Fake the necessary HTTP headers that Drupal needs: 287 | $drupal_base_url = parse_url($this->uri); 288 | // If there's no url scheme set, add http:// and re-parse the url 289 | // so the host and path values are set accurately. 290 | if (!array_key_exists('scheme', $drupal_base_url)) { 291 | $drupal_base_url = parse_url($this->uri); 292 | } 293 | // Fill in defaults. 294 | $drupal_base_url += [ 295 | 'path' => NULL, 296 | 'host' => NULL, 297 | 'port' => NULL, 298 | ]; 299 | $_SERVER['HTTP_HOST'] = $drupal_base_url['host']; 300 | 301 | if ($drupal_base_url['port']) { 302 | $_SERVER['HTTP_HOST'] .= ':' . $drupal_base_url['port']; 303 | } 304 | $_SERVER['SERVER_PORT'] = $drupal_base_url['port']; 305 | 306 | if (array_key_exists('path', $drupal_base_url)) { 307 | $_SERVER['PHP_SELF'] = $drupal_base_url['path'] . '/index.php'; 308 | } 309 | else { 310 | $_SERVER['PHP_SELF'] = '/index.php'; 311 | } 312 | } 313 | else { 314 | $_SERVER['HTTP_HOST'] = 'default'; 315 | $_SERVER['PHP_SELF'] = '/index.php'; 316 | } 317 | 318 | $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; 319 | 320 | $conf_path = DrupalKernel::findSitePath(Request::createFromGlobals()); 321 | $conf_file = $this->drupalRoot . "/$conf_path/settings.php"; 322 | if (!file_exists($conf_file)) { 323 | throw new BootstrapException(sprintf('Could not find a Drupal settings.php file at "%s"', $conf_file)); 324 | } 325 | $drushrc_file = $this->drupalRoot . "/$conf_path/drushrc.php"; 326 | if (file_exists($drushrc_file)) { 327 | require_once $drushrc_file; 328 | } 329 | } 330 | 331 | /** 332 | * {@inheritdoc} 333 | */ 334 | public function termCreate(\stdClass $term) { 335 | $term->vid = $term->vocabulary_machine_name; 336 | 337 | if (!empty($term->parent)) { 338 | $query = \Drupal::entityQuery('taxonomy_term') 339 | ->accessCheck(FALSE) 340 | ->condition('name', $term->parent) 341 | ->condition('vid', $term->vocabulary_machine_name); 342 | $parent_terms = $query->execute(); 343 | if (!empty($parent_terms)) { 344 | $term->parent = reset($parent_terms); 345 | } 346 | } 347 | 348 | $this->expandEntityFields('taxonomy_term', $term); 349 | $entity = Term::create((array) $term); 350 | $entity->save(); 351 | 352 | $term->tid = $entity->id(); 353 | return $term; 354 | } 355 | 356 | /** 357 | * {@inheritdoc} 358 | */ 359 | public function termDelete(\stdClass $term) { 360 | $term = $term instanceof TermInterface ? $term : Term::load($term->tid); 361 | if ($term instanceof TermInterface) { 362 | $term->delete(); 363 | } 364 | } 365 | 366 | /** 367 | * {@inheritdoc} 368 | */ 369 | public function getModuleList() { 370 | return array_keys(\Drupal::moduleHandler()->getModuleList()); 371 | } 372 | 373 | /** 374 | * {@inheritdoc} 375 | */ 376 | public function getExtensionPathList() { 377 | $paths = []; 378 | 379 | // Get enabled modules. 380 | foreach (\Drupal::moduleHandler()->getModuleList() as $module) { 381 | $paths[] = $this->drupalRoot . DIRECTORY_SEPARATOR . $module->getPath(); 382 | } 383 | 384 | return $paths; 385 | } 386 | 387 | /** 388 | * Expands specified base fields on the entity object. 389 | * 390 | * @param string $entity_type 391 | * The entity type for which to return the field types. 392 | * @param \StdClass $entity 393 | * Entity object. 394 | * @param array $base_fields 395 | * Base fields to be expanded in addition to user defined fields. 396 | */ 397 | public function expandEntityBaseFields($entity_type, \StdClass $entity, array $base_fields) { 398 | $this->expandEntityFields($entity_type, $entity, $base_fields); 399 | } 400 | 401 | /** 402 | * {@inheritdoc} 403 | */ 404 | public function getEntityFieldTypes($entity_type, array $base_fields = []) { 405 | $return = []; 406 | /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ 407 | $entity_field_manager = \Drupal::service('entity_field.manager'); 408 | $fields = $entity_field_manager->getFieldStorageDefinitions($entity_type); 409 | foreach ($fields as $field_name => $field) { 410 | if ($this->isField($entity_type, $field_name) 411 | || (in_array($field_name, $base_fields) && $this->isBaseField($entity_type, $field_name))) { 412 | $return[$field_name] = $field->getType(); 413 | } 414 | } 415 | return $return; 416 | } 417 | 418 | /** 419 | * {@inheritdoc} 420 | */ 421 | public function isField($entity_type, $field_name) { 422 | /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ 423 | $entity_field_manager = \Drupal::service('entity_field.manager'); 424 | $fields = $entity_field_manager->getFieldStorageDefinitions($entity_type); 425 | return (isset($fields[$field_name]) && $fields[$field_name] instanceof FieldStorageConfig); 426 | } 427 | 428 | /** 429 | * {@inheritdoc} 430 | */ 431 | public function isBaseField($entity_type, $field_name) { 432 | /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ 433 | $entity_field_manager = \Drupal::service('entity_field.manager'); 434 | $fields = $entity_field_manager->getFieldStorageDefinitions($entity_type); 435 | return (isset($fields[$field_name]) && $fields[$field_name] instanceof BaseFieldDefinition); 436 | } 437 | 438 | /** 439 | * {@inheritdoc} 440 | */ 441 | public function languageCreate(\stdClass $language) { 442 | $langcode = $language->langcode; 443 | 444 | // Enable a language only if it has not been enabled already. 445 | if (!ConfigurableLanguage::load($langcode)) { 446 | $created_language = ConfigurableLanguage::createFromLangcode($language->langcode); 447 | if (!$created_language) { 448 | throw new \InvalidArgumentException("There is no predefined language with langcode '{$langcode}'."); 449 | } 450 | $created_language->save(); 451 | return $language; 452 | } 453 | 454 | return FALSE; 455 | } 456 | 457 | /** 458 | * {@inheritdoc} 459 | */ 460 | public function languageDelete(\stdClass $language) { 461 | $configurable_language = ConfigurableLanguage::load($language->langcode); 462 | $configurable_language->delete(); 463 | } 464 | 465 | /** 466 | * {@inheritdoc} 467 | */ 468 | public function clearStaticCaches() { 469 | drupal_static_reset(); 470 | \Drupal::service('cache_tags.invalidator')->resetChecksums(); 471 | } 472 | 473 | /** 474 | * {@inheritdoc} 475 | */ 476 | public function configGet($name, $key = '') { 477 | return \Drupal::config($name)->get($key); 478 | } 479 | 480 | /** 481 | * {@inheritdoc} 482 | */ 483 | public function configGetOriginal($name, $key = '') { 484 | return \Drupal::config($name)->getOriginal($key, FALSE); 485 | } 486 | 487 | /** 488 | * {@inheritdoc} 489 | */ 490 | public function configSet($name, $key, $value) { 491 | \Drupal::configFactory()->getEditable($name) 492 | ->set($key, $value) 493 | ->save(); 494 | } 495 | 496 | /** 497 | * {@inheritdoc} 498 | */ 499 | public function entityCreate($entity_type, $entity) { 500 | // If the bundle field is empty, put the inferred bundle value in it. 501 | $bundle_key = \Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle'); 502 | if (!isset($entity->$bundle_key) && isset($entity->step_bundle)) { 503 | $entity->$bundle_key = $entity->step_bundle; 504 | } 505 | 506 | // Throw an exception if a bundle is specified but does not exist. 507 | if (isset($entity->$bundle_key) && ($entity->$bundle_key !== NULL)) { 508 | /** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */ 509 | $bundle_info = \Drupal::service('entity_type.bundle.info'); 510 | $bundles = $bundle_info->getBundleInfo($entity_type); 511 | if (!in_array($entity->$bundle_key, array_keys($bundles))) { 512 | throw new \Exception("Cannot create entity because provided bundle '$entity->$bundle_key' does not exist."); 513 | } 514 | } 515 | if (empty($entity_type)) { 516 | throw new \Exception("You must specify an entity type to create an entity."); 517 | } 518 | 519 | $this->expandEntityFields($entity_type, $entity); 520 | $createdEntity = \Drupal::entityTypeManager()->getStorage($entity_type)->create((array) $entity); 521 | $createdEntity->save(); 522 | 523 | $entity->id = $createdEntity->id(); 524 | 525 | return $entity; 526 | } 527 | 528 | /** 529 | * {@inheritdoc} 530 | */ 531 | public function entityDelete($entity_type, $entity) { 532 | $entity = $entity instanceof EntityInterface ? $entity : \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity->id); 533 | if ($entity instanceof EntityInterface) { 534 | $entity->delete(); 535 | } 536 | } 537 | 538 | /** 539 | * {@inheritdoc} 540 | */ 541 | public function startCollectingMail() { 542 | $config = \Drupal::configFactory()->getEditable('system.mail'); 543 | $data = $config->getRawData(); 544 | 545 | // Save the values for restoration after. 546 | $this->storeOriginalConfiguration('system.mail', $data); 547 | 548 | // @todo Use a collector that supports html after D#2223967 lands. 549 | $data['interface'] = ['default' => 'test_mail_collector']; 550 | $config->setData($data)->save(); 551 | // Disable the mail system module's mail if enabled. 552 | $this->startCollectingMailSystemMail(); 553 | } 554 | 555 | /** 556 | * {@inheritdoc} 557 | */ 558 | public function stopCollectingMail() { 559 | $config = \Drupal::configFactory()->getEditable('system.mail'); 560 | $config->setData($this->originalConfiguration['system.mail'])->save(); 561 | // Re-enable the mailsystem module's mail if enabled. 562 | $this->stopCollectingMailSystemMail(); 563 | } 564 | 565 | /** 566 | * {@inheritdoc} 567 | */ 568 | public function getMail() { 569 | \Drupal::state()->resetCache(); 570 | $mail = \Drupal::state()->get('system.test_mail_collector') ?: []; 571 | // Discard cancelled mail. 572 | $mail = array_values(array_filter($mail, function ($mailItem) { 573 | return ($mailItem['send'] == TRUE); 574 | })); 575 | return $mail; 576 | } 577 | 578 | /** 579 | * {@inheritdoc} 580 | */ 581 | public function clearMail() { 582 | \Drupal::state()->set('system.test_mail_collector', []); 583 | } 584 | 585 | /** 586 | * {@inheritdoc} 587 | */ 588 | public function sendMail($body = '', $subject = '', $to = '', $langcode = '') { 589 | // Send the mail, via the system module's hook_mail. 590 | $params['context']['message'] = $body; 591 | $params['context']['subject'] = $subject; 592 | $mailManager = \Drupal::service('plugin.manager.mail'); 593 | $result = $mailManager->mail('system', '', $to, $langcode, $params, NULL, TRUE); 594 | return $result; 595 | } 596 | 597 | /** 598 | * If the Mail System module is enabled, collect that mail too. 599 | * 600 | * @see MailsystemManager::getPluginInstance() 601 | */ 602 | protected function startCollectingMailSystemMail() { 603 | if (\Drupal::moduleHandler()->moduleExists('mailsystem')) { 604 | $config = \Drupal::configFactory()->getEditable('mailsystem.settings'); 605 | $data = $config->getRawData(); 606 | 607 | // Track original data for restoration. 608 | $this->storeOriginalConfiguration('mailsystem.settings', $data); 609 | 610 | // Convert all of the 'senders' to the test collector. 611 | $data = $this->findMailSystemSenders($data); 612 | $config->setData($data)->save(); 613 | } 614 | } 615 | 616 | /** 617 | * Find and replace all the mail system sender plugins with the test plugin. 618 | * 619 | * This method calls itself recursively. 620 | */ 621 | protected function findMailSystemSenders(array $data) { 622 | foreach ($data as $key => $values) { 623 | if (is_array($values)) { 624 | if (isset($values[MailsystemManager::MAILSYSTEM_TYPE_SENDING])) { 625 | $data[$key][MailsystemManager::MAILSYSTEM_TYPE_SENDING] = 'test_mail_collector'; 626 | } 627 | else { 628 | $data[$key] = $this->findMailSystemSenders($values); 629 | } 630 | } 631 | } 632 | return $data; 633 | } 634 | 635 | /** 636 | * If the Mail System module is enabled, stop collecting those mails. 637 | */ 638 | protected function stopCollectingMailSystemMail() { 639 | if (\Drupal::moduleHandler()->moduleExists('mailsystem')) { 640 | \Drupal::configFactory()->getEditable('mailsystem.settings')->setData($this->originalConfiguration['mailsystem.settings'])->save(); 641 | } 642 | } 643 | 644 | /** 645 | * {@inheritdoc} 646 | */ 647 | public function login(\stdClass $user) { 648 | $account = User::load($user->uid); 649 | \Drupal::service('account_switcher')->switchTo($account); 650 | } 651 | 652 | /** 653 | * {@inheritdoc} 654 | */ 655 | public function logout() { 656 | try { 657 | while (TRUE) { 658 | \Drupal::service('account_switcher')->switchBack(); 659 | } 660 | } 661 | catch (\RuntimeException $e) { 662 | // No more users are logged in. 663 | } 664 | } 665 | 666 | /** 667 | * Store the original value for a piece of configuration. 668 | * 669 | * If an original value has previously been stored, it is not updated. 670 | * 671 | * @param string $name 672 | * The name of the configuration. 673 | * @param mixed $value 674 | * The original value of the configuration. 675 | */ 676 | protected function storeOriginalConfiguration($name, $value) { 677 | if (!isset($this->originalConfiguration[$name])) { 678 | $this->originalConfiguration[$name] = $value; 679 | } 680 | } 681 | 682 | } 683 | -------------------------------------------------------------------------------- /src/Drupal/Driver/DriverInterface.php: -------------------------------------------------------------------------------- 1 | drupalRoot = realpath($drupal_root); 63 | if (!$this->drupalRoot) { 64 | throw new BootstrapException(sprintf('No Drupal installation found at %s', $drupal_root)); 65 | } 66 | $this->uri = $uri; 67 | $this->version = $this->getDrupalVersion(); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getRandom() { 74 | return $this->getCore()->getRandom(); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function bootstrap() { 81 | $this->getCore()->bootstrap(); 82 | $this->bootstrapped = TRUE; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function isBootstrapped() { 89 | // Assume the blackbox is always bootstrapped. 90 | return $this->bootstrapped; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function userCreate(\stdClass $user) { 97 | $this->getCore()->userCreate($user); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function userDelete(\stdClass $user) { 104 | $this->getCore()->userDelete($user); 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function processBatch() { 111 | $this->getCore()->processBatch(); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function userAddRole(\stdClass $user, $role_name) { 118 | $this->getCore()->userAddRole($user, $role_name); 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function fetchWatchdog($count = 10, $type = NULL, $severity = NULL) { 125 | throw new PendingException(sprintf('Currently no ability to access watchdog entries in %s', $this)); 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function clearCache($type = NULL) { 132 | $this->getCore()->clearCache(); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function getSubDriverPaths() { 139 | // Ensure system is bootstrapped. 140 | if (!$this->isBootstrapped()) { 141 | $this->bootstrap(); 142 | } 143 | 144 | return $this->getCore()->getExtensionPathList(); 145 | } 146 | 147 | /** 148 | * Determine major Drupal version. 149 | * 150 | * @return int 151 | * The major Drupal version. 152 | * 153 | * @throws \Drupal\Driver\Exception\BootstrapException 154 | * Thrown when the Drupal version could not be determined. 155 | * 156 | * @see drush_drupal_version() 157 | */ 158 | public function getDrupalVersion() { 159 | if (!isset($this->version)) { 160 | // Support 6, 7 and 8. 161 | $version_constant_paths = [ 162 | // Drupal 6. 163 | '/modules/system/system.module', 164 | // Drupal 7. 165 | '/includes/bootstrap.inc', 166 | // Drupal 8. 167 | '/autoload.php', 168 | '/core/includes/bootstrap.inc', 169 | ]; 170 | 171 | if ($this->drupalRoot === FALSE) { 172 | throw new BootstrapException('`drupal_root` parameter must be defined.'); 173 | } 174 | 175 | foreach ($version_constant_paths as $path) { 176 | if (file_exists($this->drupalRoot . $path)) { 177 | require_once $this->drupalRoot . $path; 178 | } 179 | } 180 | if (defined('VERSION')) { 181 | $version = VERSION; 182 | } 183 | elseif (defined('\Drupal::VERSION')) { 184 | $version = \Drupal::VERSION; 185 | } 186 | else { 187 | throw new BootstrapException('Unable to determine Drupal core version. Supported versions are 6, 7, and 8.'); 188 | } 189 | 190 | // Extract the major version from VERSION. 191 | $version_parts = explode('.', $version); 192 | if (is_numeric($version_parts[0])) { 193 | $this->version = (integer) $version_parts[0] < 8 ? $version_parts[0] : 8; 194 | } 195 | else { 196 | throw new BootstrapException(sprintf('Unable to extract major Drupal core version from version string %s.', $version)); 197 | } 198 | } 199 | return $this->version; 200 | } 201 | 202 | /** 203 | * Instantiate and set Drupal core class. 204 | * 205 | * @param array $available_cores 206 | * A major-version-keyed array of available core controllers. 207 | */ 208 | public function setCore(array $available_cores) { 209 | if (!isset($available_cores[$this->version])) { 210 | throw new BootstrapException(sprintf('There is no available Drupal core controller for Drupal version %s.', $this->version)); 211 | } 212 | $this->core = $available_cores[$this->version]; 213 | } 214 | 215 | /** 216 | * Automatically set the core from the current version. 217 | */ 218 | public function setCoreFromVersion() { 219 | $core = '\Drupal\Driver\Cores\Drupal' . $this->getDrupalVersion(); 220 | $this->core = new $core($this->drupalRoot, $this->uri); 221 | } 222 | 223 | /** 224 | * Return current core. 225 | */ 226 | public function getCore() { 227 | return $this->core; 228 | } 229 | 230 | /** 231 | * {@inheritdoc} 232 | */ 233 | public function createNode($node) { 234 | return $this->getCore()->nodeCreate($node); 235 | } 236 | 237 | /** 238 | * {@inheritdoc} 239 | */ 240 | public function nodeDelete($node) { 241 | return $this->getCore()->nodeDelete($node); 242 | } 243 | 244 | /** 245 | * {@inheritdoc} 246 | */ 247 | public function runCron() { 248 | if (!$this->getCore()->runCron()) { 249 | throw new \Exception('Failed to run cron.'); 250 | } 251 | } 252 | 253 | /** 254 | * {@inheritdoc} 255 | */ 256 | public function createTerm(\stdClass $term) { 257 | return $this->getCore()->termCreate($term); 258 | } 259 | 260 | /** 261 | * {@inheritdoc} 262 | */ 263 | public function termDelete(\stdClass $term) { 264 | return $this->getCore()->termDelete($term); 265 | } 266 | 267 | /** 268 | * {@inheritdoc} 269 | */ 270 | public function roleCreate(array $permissions) { 271 | return $this->getCore()->roleCreate($permissions); 272 | } 273 | 274 | /** 275 | * {@inheritdoc} 276 | */ 277 | public function roleDelete($rid) { 278 | $this->getCore()->roleDelete($rid); 279 | } 280 | 281 | /** 282 | * {@inheritdoc} 283 | */ 284 | public function isField($entity_type, $field_name) { 285 | return $this->getCore()->isField($entity_type, $field_name); 286 | } 287 | 288 | /** 289 | * {@inheritdoc} 290 | */ 291 | public function isBaseField($entity_type, $field_name) { 292 | return $this->getCore()->isBaseField($entity_type, $field_name); 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | */ 298 | public function languageCreate($language) { 299 | return $this->getCore()->languageCreate($language); 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function languageDelete($language) { 306 | $this->getCore()->languageDelete($language); 307 | } 308 | 309 | /** 310 | * {@inheritdoc} 311 | */ 312 | public function configGet($name, $key) { 313 | return $this->getCore()->configGet($name, $key); 314 | } 315 | 316 | /** 317 | * {@inheritdoc} 318 | */ 319 | public function configSet($name, $key, $value) { 320 | $this->getCore()->configSet($name, $key, $value); 321 | } 322 | 323 | /** 324 | * {@inheritdoc} 325 | */ 326 | public function clearStaticCaches() { 327 | $this->getCore()->clearStaticCaches(); 328 | } 329 | 330 | /** 331 | * {@inheritdoc} 332 | */ 333 | public function createEntity($entity_type, \stdClass $entity) { 334 | return $this->getCore()->entityCreate($entity_type, $entity); 335 | } 336 | 337 | /** 338 | * {@inheritdoc} 339 | */ 340 | public function entityDelete($entity_type, \stdClass $entity) { 341 | return $this->getCore()->entityDelete($entity_type, $entity); 342 | } 343 | 344 | /** 345 | * {@inheritdoc} 346 | */ 347 | public function startCollectingMail() { 348 | return $this->getCore()->startCollectingMail(); 349 | } 350 | 351 | /** 352 | * {@inheritdoc} 353 | */ 354 | public function stopCollectingMail() { 355 | return $this->getCore()->stopCollectingMail(); 356 | } 357 | 358 | /** 359 | * {@inheritdoc} 360 | */ 361 | public function getMail() { 362 | return $this->getCore()->getMail(); 363 | } 364 | 365 | /** 366 | * {@inheritdoc} 367 | */ 368 | public function clearMail() { 369 | return $this->getCore()->clearMail(); 370 | } 371 | 372 | /** 373 | * {@inheritdoc} 374 | */ 375 | public function sendMail($body, $subject, $to, $langcode) { 376 | return $this->getCore()->sendMail($body, $subject, $to, $langcode); 377 | } 378 | 379 | /** 380 | * {@inheritdoc} 381 | */ 382 | public function login(\stdClass $user) { 383 | if ($this->getCore() instanceof CoreAuthenticationInterface) { 384 | $this->getCore()->login($user); 385 | } 386 | } 387 | 388 | /** 389 | * {@inheritdoc} 390 | */ 391 | public function logout() { 392 | if ($this->getCore() instanceof CoreAuthenticationInterface) { 393 | $this->getCore()->logout(); 394 | } 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /src/Drupal/Driver/DrushDriver.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 87 | } 88 | elseif (!empty($root_path)) { 89 | $this->root = realpath($root_path); 90 | } 91 | else { 92 | throw new BootstrapException('A drush alias or root path is required.'); 93 | } 94 | 95 | $this->binary = $binary; 96 | 97 | if (!isset($random)) { 98 | $random = new Random(); 99 | } 100 | $this->random = $random; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function getRandom() { 107 | return $this->random; 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function bootstrap() { 114 | // Check that the given alias works. 115 | // @todo check that this is a functioning alias. 116 | // See http://drupal.org/node/1615450 117 | if (!isset($this->alias) && !isset($this->root)) { 118 | throw new BootstrapException('A drush alias or root path is required.'); 119 | } 120 | 121 | // Determine if drush version is legacy. 122 | if (!isset(self::$isLegacyDrush)) { 123 | self::$isLegacyDrush = $this->isLegacyDrush(); 124 | } 125 | 126 | $this->bootstrapped = TRUE; 127 | } 128 | 129 | /** 130 | * Determine if drush is a legacy version. 131 | * 132 | * @return bool 133 | * Returns TRUE if drush is older than drush 9. 134 | */ 135 | protected function isLegacyDrush() { 136 | try { 137 | // Try for a drush 9 version. 138 | $version = trim($this->drush('version', [], ['format' => 'string'])); 139 | return version_compare($version, '9', '<='); 140 | } 141 | catch (\RuntimeException $e) { 142 | // The version of drush is old enough that only `--version` was available, 143 | // so this is a legacy version. 144 | return TRUE; 145 | } 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function isBootstrapped() { 152 | return $this->bootstrapped; 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function userCreate(\stdClass $user) { 159 | $arguments = [ 160 | sprintf('"%s"', $user->name), 161 | ]; 162 | $options = [ 163 | 'password' => $user->pass, 164 | 'mail' => $user->mail, 165 | ]; 166 | $result = $this->drush('user-create', $arguments, $options); 167 | if ($uid = $this->parseUserId($result)) { 168 | $user->uid = $uid; 169 | } 170 | if (isset($user->roles) && is_array($user->roles)) { 171 | foreach ($user->roles as $role) { 172 | $this->userAddRole($user, $role); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Parse user id from drush user-information output. 179 | */ 180 | protected function parseUserId($info) { 181 | // Find the row containing "User ID : xxx". 182 | preg_match('/User ID\s+:\s+\d+/', $info, $matches); 183 | if (!empty($matches)) { 184 | // Extract the ID from the row. 185 | list(, $uid) = explode(':', $matches[0]); 186 | return (int) $uid; 187 | } 188 | } 189 | 190 | /** 191 | * {@inheritdoc} 192 | */ 193 | public function userDelete(\stdClass $user) { 194 | $arguments = [sprintf('"%s"', $user->name)]; 195 | $options = [ 196 | 'yes' => NULL, 197 | 'delete-content' => NULL, 198 | ]; 199 | $this->drush('user-cancel', $arguments, $options); 200 | } 201 | 202 | /** 203 | * {@inheritdoc} 204 | */ 205 | public function userAddRole(\stdClass $user, $role) { 206 | $arguments = [ 207 | sprintf('"%s"', $role), 208 | sprintf('"%s"', $user->name), 209 | ]; 210 | $this->drush('user-add-role', $arguments); 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function fetchWatchdog($count = 10, $type = NULL, $severity = NULL) { 217 | $options = [ 218 | 'count' => $count, 219 | 'type' => $type, 220 | 'severity' => $severity, 221 | ]; 222 | return $this->drush('watchdog-show', [], $options); 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function clearCache($type = 'all') { 229 | if (self::$isLegacyDrush) { 230 | $type = [$type]; 231 | return $this->drush('cache-clear', $type, []); 232 | } 233 | if (($type == 'all') || ($type == 'drush')) { 234 | $this->drush('cache-clear', ['drush'], []); 235 | if ($type == 'drush') { 236 | return; 237 | } 238 | } 239 | return $this->drush('cache:rebuild'); 240 | } 241 | 242 | /** 243 | * {@inheritdoc} 244 | */ 245 | public function clearStaticCaches() { 246 | // The drush driver does each operation as a separate request; 247 | // therefore, 'clearStaticCaches' can be a no-op. 248 | } 249 | 250 | /** 251 | * Decodes JSON object returned by Drush. 252 | * 253 | * It will clean up any junk that may have appeared before or after the 254 | * JSON object. This can happen with remote Drush aliases. 255 | * 256 | * @param string $output 257 | * The output from Drush. 258 | * 259 | * @return object 260 | * The decoded JSON object. 261 | */ 262 | protected function decodeJsonObject($output) { 263 | // Remove anything before the first '{'. 264 | $output = preg_replace('/^[^\{]*/', '', $output); 265 | // Remove anything after the last '}'. 266 | $output = preg_replace('/[^\}]*$/s', '', $output); 267 | return json_decode($output); 268 | } 269 | 270 | /** 271 | * {@inheritdoc} 272 | */ 273 | public function createEntity($entity_type, \StdClass $entity) { 274 | $options = [ 275 | 'entity_type' => $entity_type, 276 | 'entity' => $entity, 277 | ]; 278 | $result = $this->drush('behat', 279 | ['create-entity', escapeshellarg(json_encode($options))], []); 280 | return $this->decodeJsonObject($result); 281 | } 282 | 283 | /** 284 | * {@inheritdoc} 285 | */ 286 | public function entityDelete($entity_type, \StdClass $entity) { 287 | $options = [ 288 | 'entity_type' => $entity_type, 289 | 'entity' => $entity, 290 | ]; 291 | $this->drush('behat', 292 | ['delete-entity', escapeshellarg(json_encode($options))], []); 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | */ 298 | public function createNode($node) { 299 | // Look up author by name. 300 | if (isset($node->author)) { 301 | $user_info = $this->drush('user-information', [sprintf('"%s"', $node->author)]); 302 | if ($uid = $this->parseUserId($user_info)) { 303 | $node->uid = $uid; 304 | } 305 | } 306 | $result = $this->drush('behat', 307 | ['create-node', escapeshellarg(json_encode($node))], 308 | []); 309 | return $this->decodeJsonObject($result); 310 | } 311 | 312 | /** 313 | * {@inheritdoc} 314 | */ 315 | public function nodeDelete($node) { 316 | $this->drush('behat', ['delete-node', escapeshellarg(json_encode($node))], []); 317 | } 318 | 319 | /** 320 | * {@inheritdoc} 321 | */ 322 | public function createTerm(\stdClass $term) { 323 | $result = $this->drush('behat', 324 | [ 325 | 'create-term', 326 | escapeshellarg(json_encode($term)), 327 | ], []); 328 | return $this->decodeJsonObject($result); 329 | } 330 | 331 | /** 332 | * {@inheritdoc} 333 | */ 334 | public function termDelete(\stdClass $term) { 335 | $this->drush('behat', ['delete-term', escapeshellarg(json_encode($term))], []); 336 | } 337 | 338 | /** 339 | * {@inheritdoc} 340 | */ 341 | public function isField($entity_type, $field_name) { 342 | // If the Behat Drush Endpoint is not installed on the site-under-test, 343 | // then the drush() method will throw an exception. In this instance, we 344 | // want to treat all potential fields as non-fields. This allows the 345 | // Drush Driver to work with certain built-in Drush capabilities (e.g. 346 | // creating users) even if the Behat Drush Endpoint is not available. 347 | try { 348 | $value = [$entity_type, $field_name]; 349 | $arguments = ['is-field', escapeshellarg(json_encode($value))]; 350 | $result = $this->drush('behat', $arguments, []); 351 | return strpos($result, "true\n") !== FALSE; 352 | } 353 | catch (\Exception $e) { 354 | return FALSE; 355 | } 356 | } 357 | 358 | /** 359 | * Sets common drush arguments or options. 360 | * 361 | * @param string $arguments 362 | * Global arguments to add to every drush command. 363 | */ 364 | public function setArguments($arguments) { 365 | $this->arguments = $arguments; 366 | } 367 | 368 | /** 369 | * Get common drush arguments. 370 | */ 371 | public function getArguments() { 372 | return $this->arguments; 373 | } 374 | 375 | /** 376 | * Parse arguments into a string. 377 | * 378 | * @param array $arguments 379 | * An array of argument/option names to values. 380 | * 381 | * @return string 382 | * The parsed arguments. 383 | */ 384 | protected static function parseArguments(array $arguments) { 385 | $string = ''; 386 | foreach ($arguments as $name => $value) { 387 | if (is_null($value)) { 388 | $string .= ' --' . $name; 389 | } 390 | else { 391 | $string .= ' --' . $name . '=' . $value; 392 | } 393 | } 394 | return $string; 395 | } 396 | 397 | /** 398 | * Execute a drush command. 399 | */ 400 | public function drush($command, array $arguments = [], array $options = []) { 401 | $arguments = implode(' ', $arguments); 402 | 403 | // Disable colored output from drush. 404 | if (isset(static::$isLegacyDrush) && static::$isLegacyDrush) { 405 | $options['nocolor'] = TRUE; 406 | } 407 | else { 408 | $options['no-ansi'] = NULL; 409 | } 410 | $string_options = $this->parseArguments($options); 411 | 412 | $alias = isset($this->alias) ? "@{$this->alias}" : '--root=' . $this->root; 413 | 414 | // Add any global arguments. 415 | $global = $this->getArguments(); 416 | 417 | $cmd = "{$this->binary} {$alias} {$string_options} {$global} {$command} {$arguments}"; 418 | $process = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd); 419 | $process->setTimeout(3600); 420 | $process->run(); 421 | 422 | if (!$process->isSuccessful()) { 423 | throw new \RuntimeException($process->getErrorOutput()); 424 | } 425 | 426 | // Some drush commands write to standard error output (for example enable 427 | // use drush_log which default to _drush_print_log) instead of returning a 428 | // string (drush status use drush_print_pipe). 429 | if (!$process->getOutput()) { 430 | return $process->getErrorOutput(); 431 | } 432 | else { 433 | return $process->getOutput(); 434 | } 435 | 436 | } 437 | 438 | /** 439 | * {@inheritdoc} 440 | */ 441 | public function processBatch() { 442 | // Do nothing. Drush should internally handle any needs for processing 443 | // batch ops. 444 | } 445 | 446 | /** 447 | * {@inheritdoc} 448 | */ 449 | public function runCron() { 450 | $this->drush('cron'); 451 | } 452 | 453 | /** 454 | * Run Drush commands dynamically from a DrupalContext. 455 | */ 456 | public function __call($name, $arguments) { 457 | return $this->drush($name, $arguments); 458 | } 459 | 460 | } 461 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Exception/BootstrapException.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 33 | 34 | parent::__construct($message, $code, $previous); 35 | } 36 | 37 | /** 38 | * Returns exception driver. 39 | * 40 | * @return \Drupal\Driver\DriverInterface 41 | * The driver where the exception occurred. 42 | */ 43 | public function getDriver() { 44 | return $this->driver; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Exception/UnsupportedDriverActionException.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 59 | $this->entityType = $entity_type; 60 | $this->fieldName = $field_name; 61 | $this->fieldInfo = $this->getFieldInfo(); 62 | $this->language = $this->getEntityLanguage(); 63 | } 64 | 65 | /** 66 | * Magic caller. 67 | */ 68 | public function __call($method, $args) { 69 | if ($method == 'expand') { 70 | $args['values'] = (array) $args['values']; 71 | } 72 | return call_user_func_array([$this, $method], $args); 73 | } 74 | 75 | /** 76 | * Returns field information. 77 | * 78 | * @return array 79 | * The field array, as returned by field_read_fields(). 80 | */ 81 | public function getFieldInfo() { 82 | return field_info_field($this->fieldName); 83 | } 84 | 85 | /** 86 | * Returns the entity language. 87 | * 88 | * @return string 89 | * The entity language. 90 | */ 91 | public function getEntityLanguage() { 92 | if (field_is_translatable($this->entityType, $this->fieldInfo)) { 93 | return entity_language($this->entityType, $this->entity); 94 | } 95 | return LANGUAGE_NONE; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/DatetimeHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo['columns']['value2'])) { 16 | foreach ($values as $value) { 17 | $return[$this->language][] = [ 18 | 'value' => $value[0], 19 | 'value2' => $value[1], 20 | ]; 21 | } 22 | } 23 | else { 24 | foreach ($values as $value) { 25 | $return[$this->language][] = ['value' => $value]; 26 | } 27 | } 28 | return $return; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/DefaultHandler.php: -------------------------------------------------------------------------------- 1 | $value]; 19 | } 20 | $return[$this->language][] = $value; 21 | } 22 | return $return; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/EntityreferenceHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo['settings']['target_type']; 15 | $entity_info = entity_get_info($entity_type); 16 | // For users set label to username. 17 | if ($entity_type == 'user') { 18 | $entity_info['entity keys']['label'] = 'name'; 19 | } 20 | 21 | $return = []; 22 | foreach ($values as $value) { 23 | $target_id = db_select($entity_info['base table'], 't') 24 | ->fields('t', [$entity_info['entity keys']['id']]) 25 | ->condition('t.' . $entity_info['entity keys']['label'], $value) 26 | ->execute()->fetchField(); 27 | if ($target_id) { 28 | $return[$this->language][] = ['target_id' => $target_id]; 29 | } 30 | } 31 | return $return; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/FileHandler.php: -------------------------------------------------------------------------------- 1 | entityCondition('entity_type', 'file') 22 | ->propertyCondition('filename', $value) 23 | ->propertyOrderBy('timestamp', 'DESC') 24 | ->range(0, 1); 25 | 26 | $result = $query->execute(); 27 | 28 | if (!empty($result['file'])) { 29 | $files = entity_load('file', array_keys($result['file'])); 30 | $file = current($files); 31 | 32 | $return[$this->language][] = [ 33 | 'filename' => $file->filename, 34 | 'uri' => $file->uri, 35 | 'fid' => $file->fid, 36 | 'display' => 1, 37 | ]; 38 | } 39 | else { 40 | throw new \Exception(sprintf('File with filename "%s" not found.', $value)); 41 | } 42 | } 43 | 44 | return $return; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/ImageHandler.php: -------------------------------------------------------------------------------- 1 | language][] = [ 17 | 'title' => $value[0], 18 | 'url' => $value[1], 19 | ]; 20 | } 21 | return $return; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/ListBooleanHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo['settings']['allowed_values']; 16 | // If values are blank then use keys as value. 17 | foreach ($allowed_values as $key => $value) { 18 | if ($value == '') { 19 | $allowed_values[$key] = $key; 20 | } 21 | } 22 | $allowed_values = array_flip($allowed_values); 23 | foreach ($values as $value) { 24 | $return[$this->language][] = ['value' => $allowed_values[$value]]; 25 | } 26 | return $return; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/ListTextHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo['settings']['allowed_values_function'])) { 17 | $cacheable = TRUE; 18 | $callback = $this->fieldInfo['settings']['allowed_values_function']; 19 | $options = call_user_func($callback, $this->fieldInfo, $this, $this->entityType, $this->entity, $cacheable); 20 | } 21 | else { 22 | $options = $this->fieldInfo['settings']['allowed_values']; 23 | } 24 | foreach ($values as $value) { 25 | if (array_key_exists($value, $options)) { 26 | $allowed_values[$value] = $value; 27 | } 28 | elseif (in_array($value, $options)) { 29 | $key = array_search($value, $options); 30 | $allowed_values[$value] = $key; 31 | } 32 | } 33 | foreach ($values as $value) { 34 | $return[$this->language][] = ['value' => $allowed_values[$value]]; 35 | } 36 | return $return; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal7/TaxonomyTermReferenceHandler.php: -------------------------------------------------------------------------------- 1 | getVocab()); 17 | if (!$terms) { 18 | throw new \Exception(sprintf("No term '%s' exists.", $name)); 19 | } 20 | $return[$this->language][] = ['tid' => array_shift($terms)->tid]; 21 | } 22 | return $return; 23 | } 24 | 25 | /** 26 | * Attempt to determine the vocabulary for which the field is configured. 27 | * 28 | * @return mixed 29 | * Returns a string containing the vocabulary in which the term must be 30 | * found or NULL if unable to determine. 31 | */ 32 | protected function getVocab() { 33 | if (!empty($this->fieldInfo['settings']['allowed_values'][0]['vocabulary'])) { 34 | return $this->fieldInfo['settings']['allowed_values'][0]['vocabulary']; 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | getFieldStorageDefinitions($entity_type); 46 | $this->fieldInfo = $fields[$field_name]; 47 | 48 | // The bundle may be stored either under "step_bundle" or under the name 49 | // of the entity's bundle key. If both are empty, assume this is a single 50 | // bundle entity, and therefore make the bundle name the entity type. 51 | $bundle_key = \Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle'); 52 | $bundle = !empty($entity->$bundle_key) ? $entity->$bundle_key : (isset($entity->step_bundle) ? $entity->step_bundle : $entity_type); 53 | 54 | $fields = $entity_field_manager->getFieldDefinitions($entity_type, $bundle); 55 | $fieldsstring = ''; 56 | foreach ($fields as $key => $value) { 57 | $fieldsstring = $fieldsstring . ", " . $key; 58 | } 59 | if (empty($fields[$field_name])) { 60 | throw new \Exception(sprintf('The field "%s" does not exist on entity type "%s" bundle "%s".', $field_name, $entity_type, $bundle)); 61 | } 62 | $this->fieldConfig = $fields[$field_name]; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/AddressHandler.php: -------------------------------------------------------------------------------- 1 | fieldConfig->getSettings()['field_overrides']; 16 | $addressFields = [ 17 | "given_name" => 1, 18 | "additional_name" => 1, 19 | "family_name" => 1, 20 | "organization" => 1, 21 | "address_line1" => 1, 22 | "address_line2" => 1, 23 | "postal_code" => 1, 24 | "sorting_code" => 1, 25 | "locality" => 1, 26 | "administrative_area" => 1, 27 | ]; 28 | // Any overrides that set field inputs to hidden will be skipped. 29 | foreach ($overrides as $key => $value) { 30 | preg_match('/[A-Z]/', $key, $matches, PREG_OFFSET_CAPTURE); 31 | if (count($matches) > 0) { 32 | $fieldName = strtolower(substr_replace($key, '_', $matches[0][1], 0)); 33 | } 34 | else { 35 | $fieldName = $key; 36 | } 37 | if ($value['override'] == 'hidden') { 38 | unset($addressFields[$fieldName]); 39 | } 40 | } 41 | // The remaining field components will be populated in order, using 42 | // values as they are ordered in feature step. 43 | foreach ($values as $value) { 44 | $idx = 0; 45 | foreach ($addressFields as $k => $v) { 46 | // If the values array contains only one item, assign it to the first 47 | // field component and break. 48 | if (is_string($value)) { 49 | $return[$k] = $value; 50 | break; 51 | } 52 | if ($idx < count($value)) { 53 | // Gracefully handle users providing too few field component values. 54 | $return[$k] = $value[$idx]; 55 | $idx++; 56 | } 57 | } 58 | // Set the country code to the first available as configured in this 59 | // instance of the field. 60 | $return['country_code'] = reset($this->fieldConfig->getSettings()['available_countries']); 61 | } 62 | return [$return]; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/DatetimeHandler.php: -------------------------------------------------------------------------------- 1 | get('timezone.default')); 17 | $storageTimezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE); 18 | foreach ($values as $key => $value) { 19 | if (strpos($value, "relative:") !== FALSE) { 20 | $relative = trim(str_replace('relative:', '', $value)); 21 | // Get time, convert to ISO 8601 date in GMT/UTC, remove TZ offset. 22 | $values[$key] = substr(gmdate('c', strtotime($relative)), 0, 19); 23 | } 24 | else { 25 | // A Drupal install has a default site timezone, but nonetheless 26 | // uses UTC for internal storage. If no timezone is specified in a date 27 | // field value by the step author, assume the default timezone of 28 | // the Drupal install, and therefore transform it into UTC for storage. 29 | $date = new \DateTime($value, $siteTimezone); 30 | $date->setTimezone($storageTimezone); 31 | $values[$key] = $date->format('Y-m-d\TH:i:s'); 32 | } 33 | } 34 | return $values; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/DefaultHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo->getSetting('target_type'); 16 | $entity_definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id); 17 | 18 | $id_key = $entity_definition->getKey('id'); 19 | 20 | // Determine label field key. 21 | if ($entity_type_id !== 'user') { 22 | $label_key = $entity_definition->getKey('label'); 23 | } 24 | else { 25 | // Entity Definition->getKey('label') returns false for users. 26 | $label_key = 'name'; 27 | } 28 | 29 | if (!$label_key && $entity_type_id == 'user') { 30 | $label_key = 'name'; 31 | } 32 | 33 | // Determine target bundle restrictions. 34 | $target_bundle_key = NULL; 35 | if ($target_bundles = $this->getTargetBundles()) { 36 | $target_bundle_key = $entity_definition->getKey('bundle'); 37 | } 38 | 39 | foreach ((array) $values as $value) { 40 | $query = \Drupal::entityQuery($entity_type_id); 41 | $or = $query->orConditionGroup(); 42 | $or->condition($id_key, $value) 43 | ->condition($label_key, $value); 44 | $query->condition($or); 45 | $query->accessCheck(FALSE); 46 | if ($target_bundles && $target_bundle_key) { 47 | $query->condition($target_bundle_key, $target_bundles, 'IN'); 48 | } 49 | if ($entities = $query->execute()) { 50 | $return[] = array_shift($entities); 51 | } 52 | else { 53 | throw new \Exception(sprintf("No entity '%s' of type '%s' exists.", $value, $entity_type_id)); 54 | } 55 | } 56 | return $return; 57 | } 58 | 59 | /** 60 | * Retrieves bundles for which the field is configured to reference. 61 | * 62 | * @return mixed 63 | * Array of bundle names, or NULL if not able to determine bundles. 64 | */ 65 | protected function getTargetBundles() { 66 | $settings = $this->fieldConfig->getSettings(); 67 | if (!empty($settings['handler_settings']['target_bundles'])) { 68 | return $settings['handler_settings']['target_bundles']; 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/FileHandler.php: -------------------------------------------------------------------------------- 1 | writeData($data, 'public://' . uniqid() . '.jpg'); 21 | 22 | if (FALSE === $file) { 23 | throw new \Exception("Error saving file"); 24 | } 25 | 26 | $file->save(); 27 | 28 | $return = [ 29 | 'target_id' => $file->id(), 30 | 'alt' => 'Behat test image', 31 | 'title' => 'Behat test image', 32 | ]; 33 | return $return; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/LinkHandler.php: -------------------------------------------------------------------------------- 1 | $options, 24 | 'title' => $value[0], 25 | 'uri' => $value[1], 26 | ]; 27 | } 28 | return $return; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/ListFloatHandler.php: -------------------------------------------------------------------------------- 1 | fieldInfo->getSetting('allowed_values'); 20 | foreach ((array) $values as $value) { 21 | // Determine if a label matching the value is found. 22 | $key = array_search($value, $allowed_values); 23 | if ($key !== FALSE) { 24 | // Set the return to use the key instead of the value. 25 | $return[] = $key; 26 | } 27 | } 28 | 29 | return $return ?: $values; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/ListIntegerHandler.php: -------------------------------------------------------------------------------- 1 | getStorage('taxonomy_term') 18 | ->loadByProperties(['name' => $name]); 19 | if ($terms) { 20 | $return[] = array_shift($terms)->id(); 21 | } 22 | else { 23 | throw new \Exception(sprintf("No term '%s' exists.", $name)); 24 | } 25 | } 26 | return $return; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Drupal/Driver/Fields/Drupal8/TextWithSummaryHandler.php: -------------------------------------------------------------------------------- 1 | getMockHandler($class_name, $entity, $entity_type, $field); 34 | 35 | $field_name = $field['field_name']; 36 | $expanded_values = $handler->expand($this->values($entity->$field_name)); 37 | Assert::assertArraySubset($expected_values, $expanded_values); 38 | } 39 | 40 | /** 41 | * Data provider. 42 | * 43 | * @return array 44 | * An array of test data. 45 | */ 46 | public static function dataProvider() { 47 | return [ 48 | // Test default text field provided as simple text. 49 | [ 50 | 'DefaultHandler', 51 | (object) ['field_text' => 'Text'], 52 | 'node', 53 | ['field_name' => 'field_text'], 54 | ['en' => [['value' => 'Text']]], 55 | ], 56 | 57 | // Test default text field provided as array. 58 | [ 59 | 'DefaultHandler', 60 | (object) ['field_text' => ['Text']], 61 | 'node', 62 | ['field_name' => 'field_text'], 63 | ['en' => [['value' => 'Text']]], 64 | ], 65 | 66 | // Test default field handler using custom field columns. 67 | [ 68 | 'DefaultHandler', 69 | (object) [ 70 | 'field_addressfield' => [ 71 | [ 72 | 'country' => 'BE', 73 | 'locality' => 'Brussels', 74 | 'thoroughfare' => 'Grote Markt 1', 75 | 'postal_code' => '1000', 76 | ], 77 | ], 78 | ], 79 | 'node', 80 | ['field_name' => 'field_addressfield'], 81 | [ 82 | 'en' => [ 83 | [ 84 | 'country' => 'BE', 85 | 'locality' => 'Brussels', 86 | 'thoroughfare' => 'Grote Markt 1', 87 | 'postal_code' => '1000', 88 | ], 89 | ], 90 | ], 91 | ], 92 | 93 | // Test single-value date field provided as simple text. 94 | [ 95 | 'DatetimeHandler', 96 | (object) ['field_date' => '2015-01-01 00:00:00'], 97 | 'node', 98 | ['field_name' => 'field_date'], 99 | ['en' => [['value' => '2015-01-01 00:00:00']]], 100 | ], 101 | 102 | // Test single-value date field provided as an array. 103 | [ 104 | 'DatetimeHandler', 105 | (object) ['field_date' => ['2015-01-01 00:00:00']], 106 | 'node', 107 | ['field_name' => 'field_date'], 108 | ['en' => [['value' => '2015-01-01 00:00:00']]], 109 | ], 110 | 111 | // Test double-value date field. Can only be provided as an array 112 | // due to array type casting we perform in 113 | // \Drupal\Driver\Fields\Drupal7\AbstractFieldHandler::__call() 114 | [ 115 | 'DatetimeHandler', 116 | (object) [ 117 | 'field_date' => [ 118 | [ 119 | '2015-01-01 00:00:00', 120 | '2015-01-02 00:00:00', 121 | ], 122 | ], 123 | ], 124 | 'node', 125 | [ 126 | 'field_name' => 'field_date', 127 | 'columns' => ['value' => '', 'value2' => ''], 128 | ], 129 | [ 130 | 'en' => [ 131 | [ 132 | 'value' => '2015-01-01 00:00:00', 133 | 'value2' => '2015-01-02 00:00:00', 134 | ], 135 | ], 136 | ], 137 | ], 138 | 139 | // Test list boolean field with blank 'On' and 'Off' values. 140 | [ 141 | 'ListBooleanHandler', 142 | (object) ['field_list_boolean' => [0]], 143 | 'node', 144 | [ 145 | 'field_name' => 'field_list_boolean', 146 | 'settings' => [ 147 | 'allowed_values' => [ 148 | 0 => '', 149 | 1 => '', 150 | ], 151 | ], 152 | ], 153 | [ 154 | 'en' => [ 155 | [ 156 | 'value' => 0, 157 | ], 158 | ], 159 | ], 160 | ], 161 | ]; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /tests/Drupal/Tests/Driver/DrushDriverTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('alias', $driver->alias, 'The drush alias was not properly set.'); 20 | } 21 | 22 | /** 23 | * Tests instantiating the driver with a prefixed alias. 24 | */ 25 | public function testWithAliasPrefix() { 26 | $driver = new DrushDriver('@alias'); 27 | $this->assertEquals('alias', $driver->alias, 'The drush alias did not remove the "@" prefix.'); 28 | } 29 | 30 | /** 31 | * Tests instantiating the driver with only the root path. 32 | */ 33 | public function testWithRoot() { 34 | // Bit of a hack here to use the path to this file, but all the driver cares 35 | // about during initialization is that the root be a directory. 36 | $driver = new DrushDriver('', __FILE__); 37 | $this->assertEquals(__FILE__, $driver->root); 38 | } 39 | 40 | /** 41 | * Tests instantiating the driver with missing alias and root path. 42 | */ 43 | public function testWithNeither() { 44 | $this->expectException(BootstrapException::class); 45 | new DrushDriver('', ''); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/Drupal/Tests/Driver/FieldHandlerAbstractTestBase.php: -------------------------------------------------------------------------------- 1 | makePartial(); 41 | $mock->shouldReceive('getFieldInfo')->andReturn($field); 42 | $mock->shouldReceive('getEntityLanguage')->andReturn('en'); 43 | $mock->__construct($entity, $entity_type, $field); 44 | 45 | return $mock; 46 | } 47 | 48 | /** 49 | * Simulate __call() since mocked handlers will not run through magic methods. 50 | * 51 | * @param mixed $values 52 | * The field value(s). 53 | * 54 | * @return array 55 | * The values parameter cast to an array. 56 | */ 57 | protected function values($values) { 58 | return (array) $values; 59 | } 60 | 61 | } 62 | --------------------------------------------------------------------------------