diff --git a/frontend/drupal/modules/xautoload/.travis.yml b/frontend/drupal/modules/xautoload/.travis.yml new file mode 100644 index 000000000..23902b4d6 --- /dev/null +++ b/frontend/drupal/modules/xautoload/.travis.yml @@ -0,0 +1,5 @@ +language: php +php: + - 5.5 + - 5.4 + - 5.3 diff --git a/frontend/drupal/modules/xautoload/LICENSE.txt b/frontend/drupal/modules/xautoload/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/frontend/drupal/modules/xautoload/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/frontend/drupal/modules/xautoload/README.md b/frontend/drupal/modules/xautoload/README.md new file mode 100644 index 000000000..732a04d2a --- /dev/null +++ b/frontend/drupal/modules/xautoload/README.md @@ -0,0 +1,3 @@ + + +[![Build Status](https://travis-ci.org/donquixote/drupal-xautoload.png)](https://travis-ci.org/donquixote/drupal-xautoload) diff --git a/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/CheckIncludePath.php b/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/CheckIncludePath.php new file mode 100644 index 000000000..8b3e1dcb8 --- /dev/null +++ b/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/CheckIncludePath.php @@ -0,0 +1,17 @@ +suggestFile_checkIncludePath($path); + } +} diff --git a/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/Interface.php b/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/Interface.php new file mode 100644 index 000000000..6890b97d2 --- /dev/null +++ b/frontend/drupal/modules/xautoload/legacy/lib/FinderPlugin/Interface.php @@ -0,0 +1,51 @@ + The class finder will transform the class name to + * "Some/Namespace/Some/Class.php" + * - The plugin was registered for the namespace "Some\Namespace". This is + * because all those exotic classes all begin with Some\Namespace\ + * -> The arguments will be: + * ($api = the API object, see below) + * $logical_base_path = "Some/Namespace/" + * $relative_path = "Some/Class.php" + * $api->getClass() gives the original class name, if we still need it. + * -> We are supposed to: + * if ($api->suggestFile('exotic/location.php')) { + * return TRUE; + * } + * + * @param InjectedApiInterface $api + * An object with a suggestFile() method. + * We are supposed to suggest files until suggestFile() returns TRUE, or we + * have no more suggestions. + * @param string $logical_base_path + * The key that this plugin was registered with. + * With trailing '/'. + * @param string $relative_path + * Second part of the canonical path, ending with '.php'. + * + * @return bool|null + * TRUE, if the file was found. + * FALSE or NULL, otherwise. + */ + function findFile($api, $logical_base_path, $relative_path); +} diff --git a/frontend/drupal/modules/xautoload/legacy/lib/InjectedAPI/hookXautoload.php b/frontend/drupal/modules/xautoload/legacy/lib/InjectedAPI/hookXautoload.php new file mode 100644 index 000000000..eb9586416 --- /dev/null +++ b/frontend/drupal/modules/xautoload/legacy/lib/InjectedAPI/hookXautoload.php @@ -0,0 +1,248 @@ +finder = $adapter->getFinder(); + } + + // Prefix stuff + // --------------------------------------------------------------------------- + + /** + * Register an additional prefix for this module. + * Note: Drupal\\ is already registered for /lib. + * + * @deprecated + * + * @param string $prefix + * The prefix. + * @param string $prefix_root_dir + * Prefix root dir. + * If $relative is TRUE, this is relative to the extension module dir. + * If $relative is FALSE, this is an absolute path. + * @param boolean $relative + * Whether or not the path is relative to the current extension dir. + */ + function prefixRoot($prefix, $prefix_root_dir = NULL, $relative = TRUE) { + $prefix_root_dir = $this->processDir($prefix_root_dir, $relative); + $this->finder->registerPrefixRoot($prefix, $prefix_root_dir); + } + + /** + * Register an additional namespace for this module. + * Note: Drupal\\ is already registered for /lib. + * + * @deprecated + * + * @param string $prefix + * The namespace + * @param string $prefix_deep_dir + * PSR-0 root dir. + * If $relative is TRUE, this is relative to the current extension dir. + * If $relative is FALSE, this is an absolute path. + * @param boolean $relative + * Whether or not the path is relative to the current extension dir. + */ + function prefixDeep($prefix, $prefix_deep_dir = NULL, $relative = TRUE) { + $prefix_deep_dir = $this->processDir($prefix_deep_dir, $relative); + $this->finder->registerPrefixDeep($prefix, $prefix_deep_dir); + } + + /** + * Legacy: Plugins were called "Handler" before. + * + * @deprecated + * + * @param string $prefix + * @param xautoload_FinderPlugin_Interface $plugin + * + * @return string + * The key under which the plugin was registered. This can later be used to + * unregister the plugin again. + */ + function prefixHandler($prefix, $plugin) { + $key = Util::randomString(); + $this->finder->registerPrefixDeep($prefix, $key, $plugin); + + return $key; + } + + /** + * Register a prefix plugin object + * + * @deprecated + * + * @param string $prefix + * @param xautoload_FinderPlugin_Interface $plugin + * + * @return string + * The key under which the plugin was registered. This can later be used to + * unregister the plugin again. + */ + function prefixPlugin($prefix, $plugin) { + $key = Util::randomString(); + $this->finder->registerPrefixDeep($prefix, $key, $plugin); + + return $key; + } + + // Namespace stuff + // --------------------------------------------------------------------------- + + /** + * Register an additional namespace for this module. + * Note: Drupal\\ is already registered for /lib. + * + * @deprecated + * + * @param string $namespace + * The namespace + * @param string $psr_0_root_dir + * PSR-0 root dir. + * If $relative is TRUE, this is relative to the current module dir. + * If $relative is FALSE, this is an absolute path. + * @param boolean $relative + * Whether or not the path is relative to the current extension dir. + */ + function namespaceRoot($namespace, $psr_0_root_dir = NULL, $relative = TRUE) { + $psr_0_root_dir = $this->processDir($psr_0_root_dir, $relative); + $this->finder->registerNamespaceRoot($namespace, $psr_0_root_dir); + } + + /** + * Register an additional namespace for this module. + * Note: Drupal\\ is already registered for /lib. + * + * @deprecated + * + * @param string $namespace + * The namespace + * @param string $namespace_deep_dir + * PSR-0 root dir. + * If $relative is TRUE, this is relative to the current extension dir. + * If $relative is FALSE, this is an absolute path. + * @param boolean $relative + * Whether or not the path is relative to the current extension dir. + */ + function namespaceDeep($namespace, $namespace_deep_dir = NULL, $relative = TRUE) { + $namespace_deep_dir = $this->processDir($namespace_deep_dir, $relative); + $this->finder->registerNamespaceDeep($namespace, $namespace_deep_dir); + } + + /** + * Register a namespace plugin object + * + * @deprecated + * + * @param string $namespace + * @param xautoload_FinderPlugin_Interface $plugin + * + * @return string + * The key under which the plugin was registered. This can later be used to + * unregister the plugin again. + */ + function namespacePlugin($namespace, $plugin) { + $key = Util::randomString(); + $this->finder->registerNamespaceDeep($namespace, $key, $plugin); + + return $key; + } + + /** + * Legacy: Plugins were called "Handler" before. + * + * @deprecated + * + * @param string $namespace + * @param xautoload_FinderPlugin_Interface $plugin + * + * @return string + * The key under which the plugin was registered. This can later be used to + * unregister the plugin again. + */ + function namespaceHandler($namespace, $plugin) { + $key = Util::randomString(); + $this->finder->registerNamespaceDeep($namespace, $key, $plugin); + + return $key; + } + + /** + * Process a given directory to make it relative to Drupal root, + * instead of relative to the current extension dir. + * + * @deprecated + * + * @param string $dir + * The directory path that we want to make absolute. + * @param boolean $relative + * If TRUE, the $dir will be transformed from relative to absolute. + * If FALSE, the $dir is assumed to already be absolute, and remain unchanged. + * + * @return string + * The modified (absolute) directory path. + */ + protected function processDir($dir, $relative) { + if (!isset($dir)) { + return $this->localDirectory . 'lib/'; + } + $dir = strlen($dir) + ? rtrim($dir, '/') . '/' + : ''; + + return $relative + ? $this->localDirectory . $dir + : $dir; + } + + /** + * Explicitly set the base for relative paths. + * + * Alias for LocalDirectoryAdapter::setLocalDirectory() + * + * @param string $dir + * New relative base path. + */ + function setExtensionDir($dir) { + $this->localDirectory = strlen($dir) + ? rtrim($dir, '/') . '/' + : ''; + } +} diff --git a/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/EnvironmentSnapshotMaker.php b/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/EnvironmentSnapshotMaker.php new file mode 100644 index 000000000..4355028e5 --- /dev/null +++ b/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/EnvironmentSnapshotMaker.php @@ -0,0 +1,62 @@ + 'X Autoload unit test', + 'description' => 'Test the xautoload class finder.', + 'group' => 'X Autoload', + ); + } + + function setUp() { + + // drupal_load('module', 'xautoload') would register namespaces for all + // enabled modules, which is not intended for this unit test. + // Instead, we just include xautoload.early.inc. + require_once __DIR__ . '/../../../../xautoload.early.inc'; + + // Make sure we use the regular loader, not the APC one. + // Also make sure to prepend this one. Otherwise, the core class loader will + // try to load xautoload-related stuff, e.g. xautoload_Mock_* stuff, and + // will fail due to the database. + foreach (spl_autoload_functions() as $callback) { + if (is_array($callback) + && ($loader = $callback[0]) + && $loader instanceof ClassLoaderInterface + ) { + $loader->unregister(); + } + } + xautoload()->finder->register(TRUE); + + // Do the regular setUp(). + parent::setUp(); + } + + function testAutoloadStackOrder() { + $expected = array( + 'Drupal\\xautoload\\ClassFinder\\ClassFinder->loadClass()', + /* @see _drupal_bootstrap_database() */ + 'drupal_autoload_class', + 'drupal_autoload_interface', + /* @see simpletest_classloader_register() */ + '_simpletest_autoload_psr4_psr0', + ); + + $actual = array(); + foreach (spl_autoload_functions() as $callback) { + $actual[] = Util::callbackToString($callback); + } + + $this->assertEqualBlock($expected, $actual, "SPL autoload stack:"); + } + + function testNamespaces() { + + // Prepare the class finder. + $finder = new ClassFinder(); + $finder->add('Drupal\\ex_ample', 'sites/all/modules/contrib/ex_ample/lib-psr0'); + $finder->addPsr4('Drupal\\ex_ample', 'sites/all/modules/contrib/ex_ample/lib-psr4'); + + // Test class finding for 'Drupal\\ex_ample\\Abc_Def'. + $this->assertFinderSuggestions($finder, 'Drupal\\ex_ample\\Abc_Def', array( + // Class finder is expected to suggest these files, in the exact order, + // until one of them is accepted. + array('suggestFile', 'sites/all/modules/contrib/ex_ample/lib-psr0/Drupal/ex_ample/Abc/Def.php'), + array('suggestFile', 'sites/all/modules/contrib/ex_ample/lib-psr4/Abc_Def.php'), + )); + } + + function testPrefixes() { + + // Prepare the class finder. + $finder = new ClassFinder(); + $finder->registerPrefixDeep('ex_ample', 'sites/all/modules/contrib/ex_ample/lib'); + $finder->registerPrefixRoot('ex_ample', 'sites/all/modules/contrib/ex_ample/vendor'); + + // Test class finding for 'ex_ample_Abc_Def'. + $this->assertFinderSuggestions($finder, 'ex_ample_Abc_Def', array( + // Class finder is expected to suggest these files, in the exact order, + // until one of them is accepted. + array('suggestFile', 'sites/all/modules/contrib/ex_ample/lib/Abc/Def.php'), + array('suggestFile', 'sites/all/modules/contrib/ex_ample/vendor/ex/ample/Abc/Def.php'), + )); + } + + /** + * @param ClassFinder $finder + * @param string $class + * @param array $expectedSuggestions + * + * @return bool + * Result of the assertion + */ + protected function assertFinderSuggestions($finder, $class, array $expectedSuggestions) { + $success = TRUE; + for ($iAccept = 0; $iAccept < count($expectedSuggestions); ++$iAccept) { + list($method_name, $file) = $expectedSuggestions[$iAccept]; + $api = new CollectFilesInjectedApi($class, $method_name, $file); + $finder->apiFindFile($api, $class); + $suggestions = $api->getSuggestions(); + $expected = array_slice($expectedSuggestions, 0, $iAccept + 1); + $success = $success && $this->assertEqualBlock($expected, $suggestions, "Finder suggestions for class $class:"); + } + return $success; + } + + /** + * @param mixed $expected + * @param mixed $actual + * @param string $label + * + * @return bool + * Result of the assertion + */ + protected function assertEqualBlock($expected, $actual, $label) { + $label .= '
' . + 'Expected:
' . var_export($expected, TRUE) . '
' . + 'Actual:
' . var_export($actual, TRUE) . '
'; + return $this->assertEqual($expected, $actual, $label); + } +} diff --git a/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/XAutoloadWebTestCase.php b/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/XAutoloadWebTestCase.php new file mode 100644 index 000000000..3d80f4bd4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/lib/Drupal/xautoload/Tests/XAutoloadWebTestCase.php @@ -0,0 +1,264 @@ + 'X Autoload web test', + 'description' => 'Test xautoload class loading for an example module.', + 'group' => 'X Autoload', + ); + } + + /** + * {@inheritdoc} + */ + function setUp() { + parent::setUp(); + } + + /** + * + */ + function testNoCache() { + $this->xautoloadTestWithCacheTypes(array(), TRUE); + } + + /** + * + */ + function testApcCache() { + $cache_types = array( + 'apc' => 'apc', + 'xcache' => 'xcache', + 'wincache' => 'wincache', + ); + $this->xautoloadTestWithCacheTypes($cache_types, TRUE); + } + + /** + * @param array $cache_types + * The autoloader modes that are enabled, e.g. + * array('apc' => 'apc', 'xcache' => 'xcache') + * @param bool $cache_lazy + * Whether the "lazy" mode is enabled. + */ + protected function xautoloadTestWithCacheTypes($cache_types, $cache_lazy) { + + variable_set(XAUTOLOAD_VARNAME_CACHE_TYPES, $cache_types); + $this->pass("Set cache types: " . var_export($cache_types, TRUE)); + + variable_set(XAUTOLOAD_VARNAME_CACHE_LAZY, $cache_lazy); + $this->pass("Set cache lazy mode: " . var_export($cache_lazy, TRUE)); + + // Enable xautoload. + module_enable(array('xautoload'), FALSE); + + // At this time the xautoload_cache_mode setting is not in effect yet, + // so we have to clear old cached values from APC cache. + xautoload()->cacheManager->renewCachePrefix(); + + module_enable(array( + 'xautoload_test_1', + 'xautoload_test_2', + 'xautoload_test_3', + 'xautoload_test_4', + 'xautoload_test_5', + ), FALSE); + menu_rebuild(); + + foreach (array( + 'xautoload_test_1' => array('Drupal\xautoload_test_1\ExampleClass'), + 'xautoload_test_2' => array('xautoload_test_2_ExampleClass'), + 'xautoload_test_3' => array('Drupal\xautoload_test_3\ExampleClass'), + ) as $module => $classes) { + $classes_on_include = in_array($module, array('xautoload_test_2', 'xautoload_test_3')); + $this->xautoloadModuleEnabled($module, $classes, $classes_on_include); + $this->xautoloadModuleCheckJson($module, $cache_types, $cache_lazy, $classes); + } + } + + /** + * @param string $module + * @param string[] $classes + * @param bool $classes_on_include + */ + protected function xautoloadModuleEnabled($module, $classes, $classes_on_include) { + + EnvironmentSnapshotMaker::takeSnapshot($module, 'later', $classes); + + $all = EnvironmentSnapshotMaker::getSnapshots($module); + + foreach ($all as $phase => $observations) { + $when = ($phase === 'early') + ? 'on drupal_load() during module_enable()' + : (($phase === 'later') + ? 'after hook_modules_enabled()' + : 'at an undefined time' + ); + + // Test the classes of the example module. + foreach ($classes as $class) { + // Test that the class was already found in $phase. + $this->assertTrue(isset($observations['class_exists'][$class]), "Class $class was checked $when."); + if ($classes_on_include || $phase !== 'early') { + $this->assertTrue($observations['class_exists'][$class], "Class $class was found $when."); + } + else { + $this->assertFalse($observations['class_exists'][$class], "Class $class cannot be found $when."); + } + } + } + } + + /** + * @param string $module + * @param array $cache_types + * The autoloader modes that are enabled, e.g. + * array('apc' => 'apc', 'xcache' => 'xcache') + * @param bool $cache_lazy + * Whether the "lazy" mode is enabled. + * @param string[] $classes + */ + protected function xautoloadModuleCheckJson($module, $cache_types, $cache_lazy, $classes) { + + $path = "$module.json"; + $json = $this->drupalGet($path); + $all = json_decode($json, TRUE); + + if (!is_array($all) || empty($all)) { + $this->fail("$path must return a non-empty json array."); + return; + } + + foreach ($all as $phase => $observations) { + + $when = ($phase === 'early') + ? 'on early bootstrap' + : (($phase === 'boot') + ? 'during hook_boot()' + : 'at an undefined time' + ); + + $this->xautoloadCheckTestEnvironment($observations, $cache_types, $cache_lazy, $when); + + // Test the classes of the example module. + foreach ($classes as $class) { + // Test that the class was already found in $phase. + $this->assertTrue($observations['class_exists'][$class], "Class $class was found $when."); + } + } + } + + /** + * @param array $observations + * @param array $cache_types + * The autoloader modes that are enabled, e.g. + * array('apc' => 'apc', 'xcache' => 'xcache') + * @param bool $lazy + * Whether the "lazy" mode is enabled. + * @param $when + */ + protected function xautoloadCheckTestEnvironment($observations, $cache_types, $lazy, $when) { + + // Check early-bootstrap variables. + $label = "$when: xautoload_cache_types:"; + $this->assertEqualBlock($cache_types, $observations[XAUTOLOAD_VARNAME_CACHE_TYPES], $label); + + $label = "$when: xautoload_cache_lazy:"; + $this->assertEqualInline($lazy, $observations[XAUTOLOAD_VARNAME_CACHE_LAZY], $label); + + // Check registered class loaders. + $expected = $this->expectedAutoloadStackOrder($cache_types); + $actual = $observations['spl_autoload_functions']; + $label = "$when: spl autoload stack:"; + $this->assertEqualBlock($expected, $actual, $label); + } + + /** + * @param string $cache_types + * The autoloader modes that are enabled, e.g. + * array('apc' => 'apc', 'xcache' => 'xcache') + * + * @return string[] + * Expected order of class loaders on the spl autoload stack for the given + * autoloader mode. Each represented by a string. + */ + protected function expectedAutoloadStackOrder($cache_types) { + + if (!empty($cache_types['apc']) && extension_loaded('apc') && function_exists('apc_store')) { + $loader = 'Drupal\xautoload\ClassLoader\ApcClassLoader->loadClass()'; + } + elseif (!empty($cache_types['wincache']) && extension_loaded('wincache') && function_exists('wincache_ucache_get')) { + $loader = 'Drupal\xautoload\ClassLoader\WinCacheClassLoader->loadClass()'; + } + elseif (!empty($cache_types['xcache']) && extension_loaded('Xcache') && function_exists('xcache_get')) { + $loader = 'Drupal\xautoload\ClassLoader\XCacheClassLoader->loadClass()'; + } + else { + $loader = 'Drupal\xautoload\ClassFinder\ClassFinder->loadClass()'; + } + + return array( + 'drupal_autoload_class', + 'drupal_autoload_interface', + $loader, + ); + } + + /** + * Assert that a module is disabled. + * + * @param string $module + */ + protected function assertModuleDisabled($module) { + $this->assertFalse(module_exists($module), "Module $module is disabled."); + } + + /** + * Assert that a module is enabled. + * + * @param string $module + */ + protected function assertModuleEnabled($module) { + $this->assertTrue(module_exists($module), "Module $module is enabled."); + } + + /** + * Assert that a class is defined. + * + * @param string $class + */ + protected function assertClassExists($class) { + $this->assertTrue(class_exists($class), "Class '$class' must exist."); + } + + /** + * @param mixed $expected + * @param mixed $actual + * @param string $label + */ + protected function assertEqualBlock($expected, $actual, $label) { + $label .= + 'Expected:
' . var_export($expected, TRUE) . '
' . + 'Actual:
' . var_export($actual, TRUE) . '
'; + $this->assertEqual($expected, $actual, $label); + } + + /** + * @param mixed $expected + * @param mixed $actual + * @param string $label + */ + protected function assertEqualInline($expected, $actual, $label) { + $label .= '
' . + 'Expected: ' . var_export($expected, TRUE) . '
' . + 'Actual: ' . var_export($actual, TRUE) . ''; + $this->assertEqual($expected, $actual, $label); + } +} diff --git a/frontend/drupal/modules/xautoload/phpunit.xml.dist b/frontend/drupal/modules/xautoload/phpunit.xml.dist new file mode 100644 index 000000000..4acf8cc6d --- /dev/null +++ b/frontend/drupal/modules/xautoload/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + ./tests/src + + + diff --git a/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapter.php b/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapter.php new file mode 100644 index 000000000..e2904337d --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapter.php @@ -0,0 +1,287 @@ +finder = $finder; + $this->prefixMap = $finder->getPrefixMap(); + $this->namespaceMap = $finder->getNamespaceMap(); + $this->defaultBehavior = new DefaultDirectoryBehavior(); + $this->psr0Behavior = new Psr0DirectoryBehavior(); + $this->classMapGenerator = $classmap_generator; + } + + /** + * @return \Drupal\xautoload\ClassFinder\GenericPrefixMap + */ + function getNamespaceMap() { + return $this->namespaceMap; + } + + /** + * @return GenericPrefixMap + */ + function getPrefixMap() { + return $this->prefixMap; + } + + /** + * @return ClassMapGeneratorInterface + */ + function getClassmapGenerator() { + return $this->classMapGenerator; + } + + /** + * @return ClassMapGeneratorInterface + */ + function getFinder() { + return $this->finder; + } + + // Discovery + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addClassmapSources($paths) { + $map = $this->classMapGenerator->wildcardPathsToClassmap($paths); + $this->addClassMap($map); + } + + // Composer tools + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function composerJson($file) { + $json = ComposerJson::createFromFile($file); + $json->writeToAdapter($this); + } + + /** + * {@inheritdoc} + */ + function composerDir($dir) { + $dir = ComposerDir::create($dir); + $dir->writeToAdapter($this); + } + + // multiple PSR-0 / PSR-4 + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addMultiplePsr0(array $prefixes) { + $namespace_map = array(); + $prefix_map = array(); + foreach ($prefixes as $prefix => $paths) { + if (FALSE === strpos($prefix, '\\')) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $prefix_map[$logical_base_path][$deep_path] = $this->defaultBehavior; + } + } + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $namespace_map[$logical_base_path][$deep_path] = $this->psr0Behavior; + } + } + if (!empty($prefix_map)) { + $this->prefixMap->registerDeepPaths($prefix_map); + } + $this->namespaceMap->registerDeepPaths($namespace_map); + } + + /** + * {@inheritdoc} + */ + function addMultiplePsr4(array $map) { + $namespace_map = array(); + foreach ($map as $namespace => $paths) { + $logical_base_path = Util::namespaceLogicalPath($namespace); + foreach ($paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' + : ''; + $namespace_map[$logical_base_path][$deep_path] = $this->defaultBehavior; + } + } + $this->namespaceMap->registerDeepPaths($namespace_map); + } + + // Composer ClassLoader + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addClassMap(array $classMap) { + $this->finder->registerClasses($classMap); + } + + /** + * {@inheritdoc} + */ + function add($prefix, $paths) { + if (FALSE === strpos($prefix, '\\')) { + // Due to the ambiguity of PSR-0, this could be either PEAR-like or namespaced. + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + // Namespaced PSR-0 + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->psr0Behavior); + } + } + + /** + * {@inheritdoc} + */ + function addPsr0($prefix, $paths) { + $this->add($prefix, $paths); + } + + /** + * {@inheritdoc} + */ + function addPsr4($prefix, $paths) { + // Namespaced PSR-4 + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $deep_path) { + $deep_path = strlen($deep_path) + ? rtrim($deep_path, '/') . '/' + : ''; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + + // More convenience stuff + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addNamespacePsr0($prefix, $paths) { + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->psr0Behavior); + } + } + + /** + * {@inheritdoc} + */ + function addPear($prefix, $paths) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + + /** + * {@inheritdoc} + */ + function addPearFlat($prefix, $paths) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $deep_path) { + $deep_path = strlen($deep_path) ? (rtrim($deep_path, '/') . '/') : ''; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior + ); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapterInterface.php b/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapterInterface.php new file mode 100644 index 000000000..0fdd0d025 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Adapter/ClassFinderAdapterInterface.php @@ -0,0 +1,56 @@ +system = $system; + $this->finder = $finder; + $this->namespaceMap = $finder->getNamespaceMap(); + $this->prefixMap = $finder->getPrefixMap(); + foreach (array('module', 'theme') as $extension_type) { + $this->namespaceBehaviors[$extension_type] = new DrupalExtensionNamespaceFinderPlugin( + $extension_type, + $this->namespaceMap, + $this->prefixMap, + $this->system); + $this->prefixBehaviors[$extension_type] = new DrupalExtensionUnderscoreFinderPlugin( + $extension_type, + $this->namespaceMap, + $this->prefixMap, + $this->system); + } + $this->defaultBehavior = new DefaultDirectoryBehavior(); + } + + /** + * Register lazy plugins for enabled Drupal modules and themes, assuming that + * we don't know yet whether they use PSR-0, PEAR-Flat, or none of these. + * + * @param string[] $extensions + * An array where the keys are extension names, and the values are extension + * types like 'module' or 'theme'. + */ + function registerExtensions(array $extensions) { + + $prefix_map = array(); + $namespace_map = array(); + foreach ($extensions as $name => $type) { + if (empty($this->namespaceBehaviors[$type])) { + // Unsupported extension type, e.g. "theme_engine". + // This can happen if a site was upgraded from Drupal 6. + // See https://drupal.org/comment/8503979#comment-8503979 + continue; + } + if (!empty($this->registered[$name])) { + // The extension has already been processed. + continue; + } + $namespace_map['Drupal/' . $name . '/'][$name] = $this->namespaceBehaviors[$type]; + $prefix_map[str_replace('_', '/', $name) . '/'][$name] = $this->prefixBehaviors[$type]; + $this->registered[$name] = TRUE; + } + $this->namespaceMap->registerDeepPaths($namespace_map); + $this->prefixMap->registerDeepPaths($prefix_map); + } + + /** + * Register lazy plugins for a given extension, assuming that we don't know + * yet whether it uses PSR-0, PEAR-Flat, or none of these. + * + * @param string $name + * @param string $type + */ + function registerExtension($name, $type) { + if (!empty($this->registered[$name])) { + // The extension has already been processed. + return; + } + $this->namespaceMap->registerDeepPath('Drupal/' . $name . '/', $name, $this->namespaceBehaviors[$type]); + $this->prefixMap->registerDeepPath(str_replace('_', '/', $name) . '/', $name, $this->prefixBehaviors[$type]); + $this->registered[$name] = TRUE; + } + + /** + * Register PSR-4 directory for an extension. + * Override previous settings for this extension. + * + * @param string $name + * The extension name. + * @param string $extension_dir + * The directory of the extension. + * @param string $subdir + * The PSR-4 base directory, relative to the extension directory. + * E.g. 'lib' or 'src'. + */ + function registerExtensionPsr4($name, $extension_dir, $subdir) { + if (!empty($this->registered[$name])) { + if ('psr-4' === $this->registered[$name]) { + // It already happened. + return; + } + // Unregister the lazy plugins. + $this->namespaceMap->unregisterDeepPath('Drupal/' . $name . '/', $name); + $this->prefixMap->unregisterDeepPath(str_replace('_', '/', $name) . '/', $name); + } + + $dir = strlen($subdir) + ? $extension_dir . '/' . trim($subdir, '/') . '/' + : $extension_dir . '/'; + $this->namespaceMap->registerDeepPath('Drupal/' . $name . '/', $dir, $this->defaultBehavior); + + // Re-add the PSR-0 test directory, for consistency's sake. + if (is_dir($psr0_tests_dir = $extension_dir . '/lib/Drupal/' . $name . '/Tests')) { + $this->namespaceMap->registerDeepPath('Drupal/' . $name . '/Tests/', $psr0_tests_dir, $this->defaultBehavior); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/Adapter/LocalDirectoryAdapter.php b/frontend/drupal/modules/xautoload/src/Adapter/LocalDirectoryAdapter.php new file mode 100644 index 000000000..6949214c4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Adapter/LocalDirectoryAdapter.php @@ -0,0 +1,278 @@ +finder, $adapter->getClassmapGenerator()); + $this->master = $adapter; + $this->localDirectory = strlen($localDirectory) + ? rtrim($localDirectory, '/') . '/' + : ''; + } + + /** + * Returns an adapter object that is not relative to a local directory. + * + * @return ClassFinderAdapter + */ + function absolute() { + return $this->master; + } + + // Discovery + // --------------------------------------------------------------------------- + + /** + * Adds source paths for classmap discovery. + * + * The classmap for each source will be cached between requests. + * A "clear all caches" will trigger a rescan. + * + * @param string[] $paths + * File paths or wildcard paths for class discovery. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addClassmapSources($paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->addClassmapSources($paths); + } + + // Composer tools + // --------------------------------------------------------------------------- + + /** + * Scans a composer.json file provided by a Composer package. + * + * @param string $file + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + * + * @throws \Exception + */ + function composerJson($file, $relative = TRUE) { + $relative && $file = $this->localDirectory . $file; + $json = ComposerJson::createFromFile($file); + $json->writeToAdapter($this->master); + } + + /** + * Scans a directory containing Composer-generated autoload files. + * + * @param string $dir + * Directory to look for Composer-generated files. Typically this is the + * ../vendor/composer dir. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function composerDir($dir, $relative = TRUE) { + $relative && $dir = $this->localDirectory . $dir; + $dir = ComposerDir::create($dir); + $dir->writeToAdapter($this->master); + } + + // multiple PSR-0 / PSR-4 + // --------------------------------------------------------------------------- + + /** + * Adds multiple PSR-0 prefixes. + * + * @param array $prefixes + * Each array key is a PSR-0 prefix, e.g. "Acme\\FooPackage\\". + * Each array value is either a PSR-0 base directory or an array of PSR-0 + * base directories. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addMultiplePsr0(array $prefixes, $relative = TRUE) { + $relative && $this->prependMultiple($prefixes); + $this->master->addMultiplePsr0($prefixes); + } + + /** + * Adds multiple PSR-4 namespaces. + * + * @param array $map + * Each array key is a namespace, e.g. "Acme\\FooPackage\\". + * Each array value is either a PSR-4 base directory or an array of PSR-4 + * base directories. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addMultiplePsr4(array $map, $relative = TRUE) { + $relative && $this->prependMultiple($map); + $this->master->addMultiplePsr4($map); + } + + // Composer ClassLoader + // --------------------------------------------------------------------------- + + /** + * Registers an array ("map") of classes to file paths. + * + * @param array $classMap + * The map of classes to file paths. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addClassMap(array $classMap, $relative = TRUE) { + $relative && $this->prependToPaths($classMap); + $this->master->addClassMap($classMap); + } + + /** + * Adds a PSR-0 style prefix. Alias for ->addPsr0(). + * + * @param string $prefix + * @param string|\string[] $paths + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function add($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->add($prefix, $paths); + } + + /** + * Adds a PSR-0 style prefix. Alias for ->add(). + * + * @param string $prefix + * @param string|\string[] $paths + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addPsr0($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->add($prefix, $paths); + } + + /** + * Adds a PSR-4 style namespace. + * + * @param string $prefix + * @param string|\string[] $paths + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addPsr4($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->addPsr4($prefix, $paths); + } + + // More convenience stuff + // --------------------------------------------------------------------------- + + /** + * Adds a PSR-0 style namespace. + * + * This will assume that we are really dealing with a namespace, even if it + * has no '\\' included. + * + * @param string $prefix + * @param string|\string[] $paths + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addNamespacePsr0($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->addNamespacePsr0($prefix, $paths); + } + + /** + * Adds a PEAR-like prefix. + * + * This will assume with no further checks that $prefix contains no namespace + * separator. + * + * @param string $prefix + * The prefix, e.g. 'Acme_FooPackage_' + * @param string|string[] $paths + * An array of paths, or one specific path. + * E.g. 'lib' for $relative = TRUE, + * or 'sites/all/libraries/AcmeFooPackage/lib' for $relative = FALSE. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addPear($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->addPear($prefix, $paths); + } + + /** + * Adds a prefix similar to PEAR, but with flat directories. + * + * This will assume with no further checks that $prefix contains no namespace + * separator. + * + * @param string $prefix + * The prefix, e.g. 'Acme_FooPackage_' + * @param string|string[] $paths + * An array of paths, or one specific path. + * E.g. 'lib' for $relative = TRUE, + * or 'sites/all/libraries/AcmeFooPackage/lib' for $relative = FALSE. + * @param bool $relative + * If TRUE, the paths will be relative to $this->localDirectory. + */ + function addPearFlat($prefix, $paths, $relative = TRUE) { + $relative && $this->prependToPaths($paths); + $this->master->addPearFlat($prefix, $paths); + } + + // Relative path handling + // --------------------------------------------------------------------------- + + /** + * Prepends $this->localDirectory to a number of paths. + * + * @param array $map + */ + protected function prependMultiple(array &$map) { + foreach ($map as &$paths) { + $paths = (array) $paths; + foreach ($paths as &$path) { + $path = $this->localDirectory . $path; + } + } + } + + /** + * Prepends $this->localDirectory to a number of paths. + * + * @param string|string[] &$paths + */ + protected function prependToPaths(&$paths) { + if (!is_array($paths)) { + $paths = $this->localDirectory . $paths; + } + else { + foreach ($paths as &$path) { + $path = $this->localDirectory . $path; + } + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/CacheManager/CacheManager.php b/frontend/drupal/modules/xautoload/src/CacheManager/CacheManager.php new file mode 100644 index 000000000..633adb827 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/CacheManager/CacheManager.php @@ -0,0 +1,68 @@ +prefix = $prefix; + $this->system = $system; + } + + /** + * This method has side effects, so it is not the constructor. + * + * @param \Drupal\xautoload\DrupalSystem\DrupalSystemInterface $system + * + * @return CacheManager + */ + static function create(DrupalSystemInterface $system) { + $prefix = $system->variableGet(XAUTOLOAD_VARNAME_CACHE_PREFIX, NULL); + $manager = new self($prefix, $system); + if (empty($prefix)) { + $manager->renewCachePrefix(); + } + return $manager; + } + + /** + * @param CacheManagerObserverInterface $observer + */ + function observeCachePrefix($observer) { + $observer->setCachePrefix($this->prefix); + $this->observers[] = $observer; + } + + /** + * Renew the cache prefix, save it, and notify all observers. + */ + function renewCachePrefix() { + $this->prefix = Util::randomString(); + $this->system->variableSet(XAUTOLOAD_VARNAME_CACHE_PREFIX, $this->prefix); + foreach ($this->observers as $observer) { + $observer->setCachePrefix($this->prefix); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/CacheManager/CacheManagerObserverInterface.php b/frontend/drupal/modules/xautoload/src/CacheManager/CacheManagerObserverInterface.php new file mode 100644 index 000000000..4d13554e0 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/CacheManager/CacheManagerObserverInterface.php @@ -0,0 +1,14 @@ +loader = $loader; + } + + /** + * {@inheritdoc} + */ + function cacheMiss($finder) { + $this->loader->setFinder($finder); + } + +} diff --git a/frontend/drupal/modules/xautoload/src/CacheMissObserver/CacheMissObserverInterface.php b/frontend/drupal/modules/xautoload/src/CacheMissObserver/CacheMissObserverInterface.php new file mode 100644 index 000000000..bd1d21973 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/CacheMissObserver/CacheMissObserverInterface.php @@ -0,0 +1,29 @@ +prefixMap = new GenericPrefixMap('_'); + $this->namespaceMap = new GenericPrefixMap('\\'); + $this->defaultBehavior = new DefaultDirectoryBehavior(); + $this->psr0Behavior = new Psr0DirectoryBehavior(); + } + + /** + * {@inheritdoc} + */ + function getPrefixMap() { + return $this->prefixMap; + } + + /** + * {@inheritdoc} + */ + function getNamespaceMap() { + return $this->namespaceMap; + } + + // Composer compatibility + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addClassMap(array $classMap) { + $this->registerClasses($classMap); + } + + /** + * {@inheritdoc} + */ + function add($prefix, $paths) { + if (FALSE === strpos($prefix, '\\')) { + // Due to the ambiguity of PSR-0, this could be either PEAR-like or namespaced. + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + // Namespaced PSR-0 + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->psr0Behavior); + } + } + + /** + * {@inheritdoc} + */ + function addPsr0($prefix, $paths) { + $this->add($prefix, $paths); + } + + /** + * {@inheritdoc} + */ + function addPsr4($prefix, $paths) { + // Namespaced PSR-4 + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $deep_path) { + $deep_path = strlen($deep_path) + ? rtrim($deep_path, '/') . '/' + : ''; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + + // More convenience stuff + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function addNamespacePsr0($prefix, $paths) { + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->psr0Behavior); + } + } + + /** + * {@inheritdoc} + */ + function addPear($prefix, $paths) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior); + } + } + + /** + * {@inheritdoc} + */ + function addPearFlat($prefix, $paths) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $deep_path) { + $deep_path = strlen($deep_path) + ? (rtrim($deep_path, '/') . '/') + : ''; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $this->defaultBehavior + ); + } + } + + // Class map stuff + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function registerClass($class, $file_path) { + $this->classes[$class][$file_path] = TRUE; + } + + /** + * {@inheritdoc} + */ + function registerClasses($classes) { + foreach ($classes as $class => $file_path) { + $this->classes[$class][$file_path] = TRUE; + } + } + + // Prefix stuff + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function registerPrefixRoot($prefix, $root_path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $logical_base_path = Util::prefixLogicalPath($prefix); + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $behavior); + + if (strlen($prefix)) { + // We assume that the class named $prefix is also found at this path. + $filepath = substr($deep_path, 0, -1) . '.php'; + $this->registerClass($prefix, $filepath); + } + } + + /** + * {@inheritdoc} + */ + function registerPrefixesRoot($map, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $deep_map = array(); + foreach ($map as $prefix => $root_path) { + $logical_base_path = Util::prefixLogicalPath($prefix); + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $deep_map[$logical_base_path][$deep_path] = $behavior; + + // Register the class with name $prefix. + if (strlen($prefix)) { + $filepath = substr($deep_path, 0, -1) . '.php'; + $this->classes[$prefix][$filepath] = TRUE; + } + } + $this->prefixMap->registerDeepPaths($deep_map); + } + + /** + * {@inheritdoc} + */ + function registerPrefixDeep($prefix, $deep_path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $this->registerPrefixDeepLocation($prefix, $deep_path, $behavior); + } + + /** + * {@inheritdoc} + */ + function registerPrefixesDeep($map, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $deep_map = array(); + foreach ($map as $prefix => $deep_path) { + $logical_base_path = Util::prefixLogicalPath($prefix); + $deep_path = strlen($deep_path) + ? rtrim($deep_path, '/') . '/' + : ''; + $deep_map[$logical_base_path][$deep_path] = $behavior; + } + $this->prefixMap->registerDeepPaths($deep_map); + } + + /** + * {@inheritdoc} + */ + function registerPrefixDeepLocation($prefix, $deep_path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $logical_base_path = Util::prefixLogicalPath($prefix); + $deep_path = strlen($deep_path) + ? rtrim($deep_path, '/') . '/' + : ''; + $this->prefixMap->registerDeepPath( + $logical_base_path, + $deep_path, + $behavior); + } + + // Namespace stuff + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function registerNamespaceRoot($namespace, $root_path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $logical_base_path = Util::namespaceLogicalPath($namespace); + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $behavior); + } + + /** + * {@inheritdoc} + */ + function registerNamespacesRoot($map, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $deep_map = array(); + foreach ($map as $namespace => $root_path) { + $logical_base_path = Util::namespaceLogicalPath($namespace); + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + $deep_map[$logical_base_path][$deep_path] = $behavior; + } + $this->namespaceMap->registerDeepPaths($deep_map); + } + + /** + * {@inheritdoc} + */ + function registerNamespaceDeep($namespace, $path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $logical_base_path = Util::namespaceLogicalPath($namespace); + $deep_path = strlen($path) + ? $path . '/' + : ''; + $this->namespaceMap->registerDeepPath( + $logical_base_path, + $deep_path, + $behavior); + } + + /** + * {@inheritdoc} + */ + function registerNamespacesDeep($map, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $deep_map = array(); + foreach ($map as $namespace => $deep_path) { + $logical_base_path = Util::namespaceLogicalPath($namespace); + $deep_path = strlen($deep_path) + ? rtrim($deep_path, '/') . '/' + : ''; + $deep_map[$logical_base_path][$deep_path] = $behavior; + } + $this->namespaceMap->registerDeepPaths($deep_map); + } + + /** + * {@inheritdoc} + */ + function registerNamespaceDeepLocation($namespace, $path, $behavior = NULL) { + if (!isset($behavior)) { + $behavior = $this->defaultBehavior; + } + $namespace_path_fragment = Util::namespaceLogicalPath($namespace); + $deep_path = strlen($path) + ? $path . '/' + : ''; + $this->namespaceMap->registerDeepPath( + $namespace_path_fragment, + $deep_path, + $behavior); + } + + // --------------------------------------------------------------------------- + + /** + * {@inheritdoc} + */ + function loadClass($class) { + + // Fix the behavior of some PHP versions that prepend '\\' to the class name. + if ('\\' === $class[0]) { + $class = substr($class, 1); + } + + // First check if the literal class name is registered. + if (!empty($this->classes[$class])) { + foreach ($this->classes[$class] as $filepath => $true) { + if (file_exists($filepath)) { + require $filepath; + + return TRUE; + } + } + } + + // Check if the class has a namespace. + if (FALSE !== $pos = strrpos($class, '\\')) { + + // Build the "logical path" based on PSR-4 replacement rules. + $logical_path = str_replace('\\', '/', $class) . '.php'; + + return $this->namespaceMap->loadClass($class, $logical_path, $pos); + } + + // Build the "logical path" based on PEAR replacement rules. + $pear_logical_path = str_replace('_', '/', $class) . '.php'; + + // Clean up surplus '/' resulting from duplicate underscores, or an + // underscore at the beginning of the class. + while (FALSE !== $pos = strrpos('/' . $pear_logical_path, '//')) { + $pear_logical_path[$pos] = '_'; + } + + // Check if the class has any underscore. + $pos = strrpos($pear_logical_path, '/'); + + return $this->prefixMap->loadClass($class, $pear_logical_path, $pos); + } + + /** + * {@inheritdoc} + */ + function apiFindFile($api, $class) { + + // Fix the behavior of some PHP versions that prepend '\\' to the class name. + if ('\\' === $class[0]) { + $class = substr($class, 1); + } + + // First check if the literal class name is registered. + if (!empty($this->classes[$class])) { + foreach ($this->classes[$class] as $filepath => $true) { + if ($api->suggestFile($filepath)) { + return TRUE; + } + } + } + + // Check if the class has a namespace. + if (FALSE !== $pos = strrpos($class, '\\')) { + + // Build the "logical path" based on PSR-4 replacement rules. + $logical_path = str_replace('\\', '/', $class) . '.php'; + + return $this->namespaceMap->apiFindFile($api, $logical_path, $pos); + } + + // Build the "logical path" based on PEAR replacement rules. + $pear_logical_path = str_replace('_', '/', $class) . '.php'; + + // Clean up surplus '/' resulting from duplicate underscores, or an + // underscore at the beginning of the class. + while (FALSE !== $pos = strrpos('/' . $pear_logical_path, '//')) { + $pear_logical_path[$pos] = '_'; + } + + // Check if the class has any underscore. + $pos = strrpos($pear_logical_path, '/'); + + return $this->prefixMap->apiFindFile($api, $pear_logical_path, $pos); + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/ClassFinderInterface.php b/frontend/drupal/modules/xautoload/src/ClassFinder/ClassFinderInterface.php new file mode 100644 index 000000000..bb421d3bb --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/ClassFinderInterface.php @@ -0,0 +1,30 @@ +suggestFile($file) with all suggestions we + * can find, until it returns TRUE. Once suggestFile() returns TRUE, we stop + * and return TRUE as well. The $file will be in the $api object, so we + * don't need to return it. + * @param string $class + * The name of the class, with all namespaces prepended. + * E.g. Some\Namespace\Some\Class + * + * @return TRUE|NULL + * TRUE, if we found the file for the class. + * That is, if the $api->suggestFile($file) method returned TRUE one time. + * NULL, if we have no more suggestions. + */ + function apiFindFile($api, $class); + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/CommonRegistrationInterface.php b/frontend/drupal/modules/xautoload/src/ClassFinder/CommonRegistrationInterface.php new file mode 100644 index 000000000..6582738dd --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/CommonRegistrationInterface.php @@ -0,0 +1,81 @@ +addPsr0(). + * + * @param string $prefix + * @param string[]|string $paths + */ + function add($prefix, $paths); + + /** + * Adds a PSR-0 style prefix. Alias for ->add(). + * + * @param string $prefix + * @param string[]|string $paths + */ + function addPsr0($prefix, $paths); + + /** + * Adds a PSR-4 style namespace. + * + * @param string $prefix + * @param string[]|string $paths + */ + function addPsr4($prefix, $paths); + + // More convenience stuff + // --------------------------------------------------------------------------- + + /** + * Adds a PSR-0 style namespace. + * + * This will assume that we are really dealing with a namespace, even if it + * has no '\\' included. + * + * @param string $prefix + * @param string[]|string $paths + */ + function addNamespacePsr0($prefix, $paths); + + /** + * Adds a PEAR-like prefix. + * + * This will assume with no further checks that $prefix contains no namespace + * separator. + * + * @param string $prefix + * @param string[]|string $paths + */ + function addPear($prefix, $paths); + + /** + * Adds a prefix similar to PEAR, but with flat directories. + * + * This will assume with no further checks that $prefix contains no namespace + * separator. + * + * @param string $prefix + * @param string[]|string $paths + */ + function addPearFlat($prefix, $paths); + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/ExtendedClassFinderInterface.php b/frontend/drupal/modules/xautoload/src/ClassFinder/ExtendedClassFinderInterface.php new file mode 100644 index 000000000..c5a8b4913 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/ExtendedClassFinderInterface.php @@ -0,0 +1,215 @@ + ../lib/My/Prefix/SomeClass.php + * My_Prefix -> ../lib/My/Prefix.php + * @param DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerPrefixRoot($prefix, $root_path, $behavior = NULL); + + /** + * Register an array of PEAR-style deep paths for given class prefixes. + * + * Note: + * This actually goes beyond PEAR style, because it also allows "shallow" + * PEAR-like structures like + * my_library_Some_Class -> (library dir)/src/Some/Class.php + * instead of + * my_library_Some_Class -> (library dir)/src/my/library/Some/Class.php + * via + * $finder->registerPrefixDeep('my_library', "$library_dir/src"); + * + * @param string[] $map + * Associative array, the keys are the prefixes, the values are the + * directories. + * This does NOT cover the class named $prefix itself. + * @param DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerPrefixesRoot($map, $behavior = NULL); + + /** + * Register a PEAR-style deep path for a given class prefix. + * + * Note: + * This actually goes beyond PEAR style, because it also allows things like + * my_library_Some_Class -> (library dir)/src/Some/Class.php + * instead of + * my_library_Some_Class -> (library dir)/src/my/library/Some/Class.php + * via + * $finder->registerPrefixDeep('my_library', "$library_dir/src"); + * + * @param string $prefix + * Prefix, e.g. "My_Prefix", for classes like "My_Prefix_SomeClass". + * This does NOT cover the class named "My_Prefix" itself. + * @param string $deep_path + * The deep path, e.g. "../lib/My/Prefix", for classes placed in + * My_Prefix_SomeClass -> ../lib/My/Prefix/SomeClass.php + * @param DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerPrefixDeep($prefix, $deep_path, $behavior = NULL); + + /** + * Register an array of PEAR-style deep paths for given class prefixes. + * + * Note: + * This actually goes beyond PEAR style, because it also allows "shallow" + * PEAR-like structures like + * my_library_Some_Class -> (library dir)/src/Some/Class.php + * instead of + * my_library_Some_Class -> (library dir)/src/my/library/Some/Class.php + * via + * $finder->registerPrefixDeep('my_library', "$library_dir/src"); + * + * @param string[] $map + * Associative array, the keys are the prefixes, the values are the + * directories. + * This does NOT cover the class named $prefix itself. + * @param \Drupal\xautoload\DirectoryBehavior\DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerPrefixesDeep($map, $behavior = NULL); + + /** + * Register a filesystem location for a given class prefix. + * + * @param string $prefix + * The prefix, e.g. "My_Prefix" + * @param string $deep_path + * The deep filesystem location, e.g. "../lib/My/Prefix". + * @param DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerPrefixDeepLocation($prefix, $deep_path, $behavior = NULL); + + // Namespace stuff + // --------------------------------------------------------------------------- + + /** + * Register a PSR-0 root folder for a given namespace. + * + * @param string $namespace + * The namespace, e.g. "My\Namespace", to cover all classes within that, + * e.g. My\Namespace\SomeClass, or My\Namespace\Xyz\SomeClass. This does not + * cover the root-level class, e.g. My\Namespace + * @param string $root_path + * The deep path, e.g. "../lib", if classes reside in e.g. + * My\Namespace\SomeClass -> ../lib/My/Namespace/SomeClass.php + * @param \Drupal\xautoload\DirectoryBehavior\DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerNamespaceRoot($namespace, $root_path, $behavior = NULL); + + /** + * Register PSR-0 root folders for given namespaces. + * + * @param string[] $map + * Associative array, the keys are the namespaces, the values are the + * directories. + * @param \Drupal\xautoload\DirectoryBehavior\DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerNamespacesRoot($map, $behavior = NULL); + + /** + * Alias for registerNamespaceDeepLocation() + * + * @param string $namespace + * The namespace, e.g. "My\Namespace" + * @param string $path + * The deep path, e.g. "../lib/My/Namespace" + * @param \Drupal\xautoload\DirectoryBehavior\DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerNamespaceDeep($namespace, $path, $behavior = NULL); + + /** + * Register a number of "deep" namespace directories at once. + * + * @param string[] $map + * @param DirectoryBehaviorInterface $behavior + */ + function registerNamespacesDeep($map, $behavior = NULL); + + /** + * Register a deep filesystem location for a given namespace. + * + * @param string $namespace + * The namespace, e.g. "My\Namespace" + * @param string $path + * The deep path, e.g. "../lib/My/Namespace" + * @param DirectoryBehaviorInterface $behavior + * If TRUE, then we are not sure if the directory at $path actually exists. + * If during the process we find the directory to be nonexistent, we + * unregister the path. + */ + function registerNamespaceDeepLocation($namespace, $path, $behavior = NULL); +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/GenericPrefixMap.php b/frontend/drupal/modules/xautoload/src/ClassFinder/GenericPrefixMap.php new file mode 100644 index 000000000..9f2e5f604 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/GenericPrefixMap.php @@ -0,0 +1,242 @@ +separator = $separator; + } + + /** + * If a class file would be in + * $psr0_root . '/' . $path_fragment . $path_suffix + * then instead, we look in + * $deep_path . $path_suffix + * + * @param string $logical_base_path + * The would-be namespace path relative to PSR-0 root. + * That is, the namespace with '\\' replaced by '/'. + * @param string $deep_path + * The filesystem location of the (PSR-0) subfolder for the given namespace. + * @param DirectoryBehaviorInterface $behavior + * Behavior in this directory. + */ + function registerDeepPath($logical_base_path, $deep_path, $behavior) { + $this->paths[$logical_base_path][$deep_path] = $behavior; + } + + /** + * @param string $logical_base_path + * The would-be namespace path relative to PSR-0 root. + * That is, the namespace with '\\' replaced by '/'. + * @param string $deep_path + * The filesystem location of the (PSR-0) subfolder for the given namespace. + * @param DirectoryBehaviorInterface $behavior + * Behavior in this directory. + */ + function prependDeepPath($logical_base_path, $deep_path, $behavior) { + $this->paths[$logical_base_path] + = isset($this->paths[$logical_base_path]) + ? array($deep_path => $behavior) + $this->paths[$logical_base_path] + : array($deep_path => $behavior); + } + + /** + * Register a bunch of those paths .. + * + * @param array[] $map + * + * @throws \Exception + */ + function registerDeepPaths(array $map) { + foreach ($map as $key => $paths) { + if (isset($this->paths[$key])) { + $paths += $this->paths[$key]; + } + $this->paths[$key] = $paths; + } + } + + /** + * Delete a registered path mapping. + * + * @param string $logical_base_path + * @param string $deep_path + */ + function unregisterDeepPath($logical_base_path, $deep_path) { + unset($this->paths[$logical_base_path][$deep_path]); + } + + + /** + * @param string $class + * @param string $logical_path + * Class name translated into a logical path, either with PSR-4 or with PEAR + * translation rules. + * @param int|bool $lastpos + * Position of the last directory separator in $logical_path. + * FALSE, if there is no directory separator in $logical_path. + * + * @return bool|NULL + * TRUE, if the class was found. + */ + function loadClass($class, $logical_path, $lastpos) { + $pos = $lastpos; + while (TRUE) { + $logical_base_path = (FALSE === $pos) + ? '' + : substr($logical_path, 0, $pos + 1); + + if (isset($this->paths[$logical_base_path])) { + foreach ($this->paths[$logical_base_path] as $dir => $behavior) { + if ($behavior instanceof DefaultDirectoryBehavior) { + // PSR-4 and PEAR + if (file_exists($file = $dir . substr($logical_path, $pos + 1))) { + require $file; + + return TRUE; + } + } + elseif ($behavior instanceof Psr0DirectoryBehavior) { + // PSR-0 + if (file_exists( + $file = $dir + . substr($logical_path, $pos + 1, $lastpos - $pos) + . str_replace('_', '/', substr($logical_path, $lastpos + 1)) + )) { + require $file; + + return TRUE; + } + } + elseif ($behavior instanceof xautoload_FinderPlugin_Interface) { + // Legacy "FinderPlugin". + $api = new LoadClassInjectedAPI($class); + if ($behavior->findFile($api, $logical_base_path, substr($logical_path, $pos + 1), $dir)) { + return TRUE; + } + } + } + } + + // Continue with parent fragment. + if (FALSE === $pos) { + return NULL; + } + + $pos = strrpos($logical_base_path, '/', -2); + } + + return NULL; + } + + /** + * Find the file for a class that in PSR-0 or PEAR would be in + * $psr_0_root . '/' . $path_fragment . $path_suffix + * + * @param InjectedApiInterface $api + * @param string $logical_path + * Class name translated into a logical path, either with PSR-4 or with PEAR + * translation rules. + * @param int|bool $lastpos + * Position of the last directory separator in $logical_path. + * FALSE, if there is no directory separator in $logical_path. + * + * @return bool|NULL + * TRUE, if the class was found. + */ + function apiFindFile($api, $logical_path, $lastpos) { + $pos = $lastpos; + while (TRUE) { + $logical_base_path = (FALSE === $pos) + ? '' + : substr($logical_path, 0, $pos + 1); + + if (isset($this->paths[$logical_base_path])) { + foreach ($this->paths[$logical_base_path] as $dir => $behavior) { + if ($behavior instanceof DefaultDirectoryBehavior) { + // PSR-4 and PEAR + if ($api->suggestFile($dir . substr($logical_path, $pos + 1))) { + return TRUE; + } + } + elseif ($behavior instanceof Psr0DirectoryBehavior) { + // PSR-0 + if ($api->suggestFile( + $dir + . substr($logical_path, $pos + 1, $lastpos - $pos) + . str_replace('_', '/', substr($logical_path, $lastpos + 1)) + )) { + return TRUE; + } + } + elseif ($behavior instanceof xautoload_FinderPlugin_Interface) { + // Legacy "FinderPlugin". + if ($behavior->findFile($api, $logical_base_path, substr($logical_path, $pos + 1), $dir)) { + return TRUE; + } + } + } + } + + // Continue with parent fragment. + if (FALSE === $pos) { + return NULL; + } + + $pos = strrpos($logical_base_path, '/', -2); + } + + return NULL; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/AbstractInjectedApi.php b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/AbstractInjectedApi.php new file mode 100644 index 000000000..f4aba9643 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/AbstractInjectedApi.php @@ -0,0 +1,50 @@ +className = $class_name; + } + + /** + * This is done in the injected api object, so we can easily provide a mock + * implementation. + */ + function is_dir($dir) { + return is_dir($dir); + } + + /** + * Get the name of the class we are looking for. + * + * @return string + * The class we are looking for. + */ + function getClass() { + return $this->className; + } + + /** + * Dummy method to force autoloading this class (or an ancestor). + */ + static function forceAutoload() { + // Do nothing. + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/CollectFilesInjectedApi.php b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/CollectFilesInjectedApi.php new file mode 100644 index 000000000..b62ef29b1 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/CollectFilesInjectedApi.php @@ -0,0 +1,147 @@ +file, will return TRUE. + */ + protected $methodName; + + /** + * @var string + * The file where $this->$method($this->file) will return TRUE. + */ + protected $file; + + /** + * @var array[] + * All files that were suggested. + */ + protected $suggestions; + + /** + * @param string $class_name + * @var string $method + * The method that, if called with $this->file, will return TRUE. + * @param string $file + * The file where $this->$method($this->file) will return TRUE. + */ + function __construct($class_name, $method_name, $file) { + $this->methodName = $method_name; + $this->file = $file; + parent::__construct($class_name); + } + + /** + * When the process has finished, use this to return the result. + * + * @return string + * The file that is supposed to declare the class. + */ + function getSuggestions() { + return $this->suggestions; + } + + /** + * Suggest a file that, if the file exists, + * has to declare the class we are looking for. + * Only keep the class on success. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file exists. + * FALSE, otherwise. + */ + function suggestFile($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return __FUNCTION__ === $this->methodName && $file === $this->file; + } + + /** + * Same as suggestFile(), but skip the file_exists(), + * assuming that we already know the file exists. + * + * This could make sense if a plugin already did the file_exists() check. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_skipFileExists($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return TRUE; + } + + /** + * Same as suggestFile(), but assume that file_exists() returns TRUE. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_checkNothing($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return TRUE; + } + + /** + * Same as suggestFile(), but check the full PHP include path. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file exists. + * FALSE, otherwise. + */ + function suggestFile_checkIncludePath($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return __FUNCTION__ == $this->methodName && $file === $this->file; + } + + /** + * {@inheritdoc} + */ + function guessFile($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return __FUNCTION__ == $this->methodName && $file === $this->file; + } + + /** + * {@inheritdoc} + */ + function guessPath($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return __FUNCTION__ == $this->methodName && $file === $this->file; + } + + /** + * {@inheritdoc} + */ + function claimFile($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return TRUE; + } + + /** + * {@inheritdoc} + */ + function claimPath($file) { + $this->suggestions[] = array(__FUNCTION__, $file); + return __FUNCTION__ == $this->methodName && $file === $this->file; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/FindFileInjectedApi.php b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/FindFileInjectedApi.php new file mode 100644 index 000000000..67107cd3e --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/FindFileInjectedApi.php @@ -0,0 +1,145 @@ +file; + } + + /** + * Suggest a file that, if the file exists, + * has to declare the class we are looking for. + * Only keep the class on success. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file exists. + * FALSE, otherwise. + */ + function suggestFile($file) { + if (file_exists($file)) { + $this->file = $file; + return TRUE; + } + return FALSE; + } + + /** + * Same as suggestFile(), but skip the file_exists(), + * assuming that we already know the file exists. + * + * This could make sense if a plugin already did the file_exists() check. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_skipFileExists($file) { + $this->file = $file; + return TRUE; + } + + /** + * Same as suggestFile(), but assume that file_exists() returns TRUE. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_checkNothing($file) { + $this->file = $file; + return TRUE; + } + + /** + * Same as suggestFile(), but check the full PHP include path. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file exists. + * FALSE, otherwise. + */ + function suggestFile_checkIncludePath($file) { + if (FALSE !== $file = Util::findFileInIncludePath($file)) { + $this->file = $file; + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function guessFile($file) { + // The file must be included, or else we can't know if it defines the class. + require_once $file; + if (Util::classIsDefined($this->className)) { + $this->file = $file; + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function guessPath($file) { + if (file_exists($file)) { + // The file must be included, or else we can't know if it defines the class. + require_once $file; + if (Util::classIsDefined($this->className)) { + $this->file = $file; + return TRUE; + } + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function claimFile($file) { + $this->file = $file; + return TRUE; + } + + /** + * {@inheritdoc} + */ + function claimPath($file) { + if (file_exists($file)) { + $this->file = $file; + return TRUE; + } + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/InjectedApiInterface.php b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/InjectedApiInterface.php new file mode 100644 index 000000000..6e836e90e --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/InjectedApiInterface.php @@ -0,0 +1,138 @@ +file = $file; + require $file; + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Same as suggestFile(), but skip the file_exists(), + * assuming that we already know the file exists. + * + * This could make sense if a plugin already did the file_exists() check. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_skipFileExists($file) { + $this->file = $file; + require $file; + return TRUE; + } + + /** + * Same as suggestFile(), but assume that file_exists() returns TRUE. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file was found - which is always. + */ + function suggestFile_checkNothing($file) { + $this->file = $file; + require $file; + return TRUE; + } + + /** + * Same as suggestFile(), but check the full PHP include path. + * + * @param string $file + * The file that is supposed to declare the class. + * + * @return bool + * TRUE, if the file exists. + * FALSE, otherwise. + */ + function suggestFile_checkIncludePath($file) { + if (FALSE !== $file = Util::findFileInIncludePath($file)) { + $this->file = $file; + require $file; + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function guessFile($file) { + require_once $file; + if (Util::classIsDefined($this->className)) { + $this->file = $file; + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function guessPath($file) { + if (file_exists($file)) { + require_once $file; + if (Util::classIsDefined($this->className)) { + $this->file = $file; + return TRUE; + } + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + function claimFile($file) { + require $file; + $this->file = $file; + return TRUE; + } + + /** + * {@inheritdoc} + */ + function claimPath($file) { + if (file_exists($file)) { + require $file; + $this->file = $file; + return TRUE; + } + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/LoadClassInjectedAPI.php b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/LoadClassInjectedAPI.php new file mode 100644 index 000000000..a233628cb --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/InjectedApi/LoadClassInjectedAPI.php @@ -0,0 +1,105 @@ +className); + } + + /** + * {@inheritdoc} + */ + function guessPath($file) { + if (file_exists($file)) { + require_once $file; + + return Util::classIsDefined($this->className); + } + else { + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + function claimFile($file) { + require $file; + + return TRUE; + } + + /** + * {@inheritdoc} + */ + function claimPath($file) { + if (file_exists($file)) { + require $file; + + return TRUE; + } + else { + return FALSE; + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalCoreRegistryPlugin.php b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalCoreRegistryPlugin.php new file mode 100644 index 000000000..50b45f033 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalCoreRegistryPlugin.php @@ -0,0 +1,79 @@ +baseDir = $baseDir; + } + + /** + * Find the file for a class that in PSR-0 or PEAR would be in + * $psr_0_root . '/' . $path_fragment . $path_suffix + * + * E.g.: + * - The class we look for is Some\Namespace\Some\Class + * - The file is actually in "exotic/location.php". This is not following + * PSR-0 or PEAR standard, so we need a plugin. + * -> The class finder will transform the class name to + * "Some/Namespace/Some/Class.php" + * - The plugin was registered for the namespace "Some\Namespace". This is + * because all those exotic classes all begin with Some\Namespace\ + * -> The arguments will be: + * ($api = the API object, see below) + * $logical_base_path = "Some/Namespace/" + * $relative_path = "Some/Class.php" + * $api->getClass() gives the original class name, if we still need it. + * -> We are supposed to: + * if ($api->suggestFile('exotic/location.php')) { + * return TRUE; + * } + * + * @param InjectedApiInterface $api + * An object with a suggestFile() method. + * We are supposed to suggest files until suggestFile() returns TRUE, or we + * have no more suggestions. + * @param string $logical_base_path_empty + * The key that this plugin was registered with. + * With trailing '/'. + * @param string $relative_path_irrelevant + * Second part of the canonical path, ending with '.php'. + * + * @return bool|null + * TRUE, if the file was found. + * FALSE or NULL, otherwise. + */ + function findFile($api, $logical_base_path_empty, $relative_path_irrelevant) { + $q = db_select('registry'); + // Use LIKE here to make the query case-insensitive. + $q->condition('name', db_like($api->getClass()), 'LIKE'); + $q->addField('registry', 'filename'); + $stmt = $q->execute(); + while ($relative_path = $stmt->fetchField()) { + $file = $this->baseDir . $relative_path; + // Attention: The db_select() above can trigger the class loader for + // classes and interfaces of the database layer. This can cause some files + // to be included twice, if the file defines more than one class. + // So we need to use require_once here, instead of require. That is, use + // guessFile() instead of claimFile(). + if ($api->guessFile($file)) { + return TRUE; + } + } + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionNamespaceFinderPlugin.php b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionNamespaceFinderPlugin.php new file mode 100644 index 000000000..0902673be --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionNamespaceFinderPlugin.php @@ -0,0 +1,219 @@ +type = $type; + $this->prefixMap = $prefix_map; + $this->namespaceMap = $namespace_map; + $this->defaultBehavior = new DefaultDirectoryBehavior(); + $this->psr0Behavior = new Psr0DirectoryBehavior(); + $this->system = $system; + } + + /** + * Looks up a class starting with "Drupal\$extension_name\\". + * + * This plugin method will be called for every class beginning with + * "Drupal\\$extension_name\\", as long as the plugin is registered for + * $logical_base_path = 'Drupal/$extension_name/'. + * + * A similar plugin will is registered along with this one for the PEAR-FLAT + * pattern, called for every class beginning with $modulename . '_'. + * + * The plugin will eventually unregister itself and its cousin, once it has + * - determined the correct path for the module, and + * - determined that the module is using either PSR-0 or PSR-4. + * It does that by including the file candidate for PSR-0 and/or PSR-4 and + * checking whether the class is now defined. + * + * The plugin will instead register a direct + * + * @param \Drupal\xautoload\ClassFinder\InjectedApi\InjectedApiInterface $api + * An object with methods like suggestFile() and guessFile(). + * @param string $logical_base_path + * The logical base path determined from the registered namespace. + * E.g. 'Drupal/menupoly/'. + * @param string $relative_path + * Remaining part of the logical path following $logical_base_path. + * E.g. 'FooNamespace/BarClass.php'. + * @param string|null $extension_name + * Second key that the plugin was registered with. Usually this would be the + * physical base directory where we prepend the relative path to get the + * file path. But in this case it is simply the extensions name. + * E.g. 'menupoly'. + * + * @return bool|null + * TRUE, if the file was found. + * FALSE or NULL, otherwise. + */ + function findFile($api, $logical_base_path, $relative_path, $extension_name = NULL) { + + $extension_file = $this->system->drupalGetFilename($this->type, $extension_name); + if (empty($extension_file)) { + // Extension does not exist, or is not installed. + return FALSE; + } + + $nspath = 'Drupal/' . $extension_name . '/'; + $testpath = $nspath . 'Tests/'; + $uspath = $extension_name . '/'; + $extension_dir = dirname($extension_file); + $src = $extension_dir . '/src/'; + $lib_psr0 = $extension_dir . '/lib/Drupal/' . $extension_name . '/'; + $is_test_class = (0 === strpos($relative_path, 'Tests/')); + + // Try PSR-4. + if ($api->guessPath($src . $relative_path)) { + if ($is_test_class) { + // Register PSR-0 directory for "Drupal\\$modulename\\Tests\\" + // This generally happens only once per module, because for subsequent + // test classes the class will be found before this plugin is triggered. + // However, for class_exists() with nonexistent test files, this line + // will occur more than once. + $this->namespaceMap->registerDeepPath($testpath, $src . 'Tests/', $this->defaultBehavior); + // We found the class, but it is a test class, so it does not tell us + // anything about whether non-test classes are in PSR-0 or PSR-4. + return TRUE; + } + + // Register PSR-4 directory for "Drupal\\$modulename\\". + $this->namespaceMap->registerDeepPath($nspath, $src, $this->defaultBehavior); + + // Unregister the lazy plugins, including this one, for + // "Drupal\\$modulename\\" and for $modulename . '_'. + $this->namespaceMap->unregisterDeepPath($nspath, $extension_name); + $this->prefixMap->unregisterDeepPath($uspath, $extension_name); + + // Test classes in PSR-4 are already covered by the PSR-4 plugin we just + // registered. But test classes in PSR-0 would slip through. So we check + // if a separate behavior needs to be registered for those. + if (is_dir($lib_psr0 . 'Tests/')) { + $this->namespaceMap->registerDeepPath($testpath, $lib_psr0 . 'Tests/', $this->psr0Behavior); + } + + // The class was found, so return TRUE. + return TRUE; + } + + // Build PSR-0 relative path. + if (FALSE === $nspos = strrpos($relative_path, '/')) { + // No namespace separators in $relative_path, so all underscores must be + // replaced. + $relative_path = str_replace('_', '/', $relative_path); + } + else { + // Replace only those underscores in $relative_path after the last + // namespace separator, from right to left. On average there is no or very + // few of them, so this loop rarely iterates even once. + while ($nspos < $uspos = strrpos($relative_path, '_')) { + $relative_path[$uspos] = '/'; + } + } + + // Try PSR-0 + if ($api->guessPath($lib_psr0 . $relative_path)) { + if ($is_test_class) { + // We know now that there are test classes using PSR-0. + $this->namespaceMap->registerDeepPath($testpath, $lib_psr0 . 'Tests/', $this->psr0Behavior); + // We found the class, but it is a test class, so it does not tell us + // anything about whether non-test classes are in PSR-0 or PSR-4. + return TRUE; + } + + // Unregister the lazy plugins, including this one. + $this->namespaceMap->unregisterDeepPath($nspath, $extension_name); + $this->prefixMap->unregisterDeepPath($uspath, $extension_name); + + // Register PSR-0 for regular namespaced classes. + $this->namespaceMap->registerDeepPath($nspath, $lib_psr0, $this->psr0Behavior); + + // Test classes in PSR-0 are already covered by the PSR-0 plugin we just + // registered. But test classes in PSR-4 would slip through. So we check + // if a separate behavior needs to be registered for those. + # if (is_dir($src . 'Tests/')) { + # $this->namespaceMap->registerDeepPath($testpath, $src . 'Tests/', $this->psr0Behavior); + # } + + // The class was found, so return TRUE. + return TRUE; + } + + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionUnderscoreFinderPlugin.php b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionUnderscoreFinderPlugin.php new file mode 100644 index 000000000..794653658 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/DrupalExtensionUnderscoreFinderPlugin.php @@ -0,0 +1,53 @@ +system->drupalGetFilename($this->type, $extension_name); + + if (empty($extension_file)) { + // Extension does not exist, or is not installed. + return FALSE; + } + + $nspath = 'Drupal/' . $extension_name . '/'; + $testpath = $nspath . 'Tests/'; + $uspath = $extension_name . '/'; + $lib = dirname($extension_file) . '/lib/'; + $lib_psr0 = $lib . 'Drupal/' . $extension_name . '/'; + + // Try PEAR-Flat. + if ($api->guessPath($lib . $relative_path)) { + // Register PEAR-Flat. + $this->prefixMap->registerDeepPath($uspath, $lib, $this->defaultBehavior); + // Unregister the lazy plugins. + $this->namespaceMap->unregisterDeepPath($nspath, $extension_name); + $this->prefixMap->unregisterDeepPath($uspath, $extension_name); + // See if there are PSR-0 or PSR-4 test classes. + if (is_dir($lib_psr0 . 'Tests/')) { + $this->namespaceMap->registerDeepPath( + $testpath, + $lib_psr0 . 'Tests/', + $this->psr0Behavior); + } + if (is_dir($lib . 'Tests/')) { + $this->namespaceMap->registerDeepPath( + $testpath, + $lib . 'Tests/', + $this->defaultBehavior); + } + + // The class was found, so return TRUE. + return TRUE; + } + + // The class was not found, so return FALSE. + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/FinderPluginInterface.php b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/FinderPluginInterface.php new file mode 100644 index 000000000..4803d0997 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/FinderPluginInterface.php @@ -0,0 +1,65 @@ + The class finder will transform the class name to + * "Some/Namespace/Some/Class.php" + * - The plugin was registered for the namespace "Some\Namespace". This is + * because all those exotic classes all begin with Some\Namespace\ + * -> The arguments will be: + * ($api = the API object, see below) + * $path_fragment = "Some/Namespace/" + * $path_suffix = "Some/Class.php" + * $api->getClass() gives the original class name, if we still need it. + * -> We are supposed to: + * if ($api->suggestFile('exotic/location.php')) { + * return TRUE; + * } + * + * @param InjectedApiInterface $api + * An object with a suggestFile() method. + * We are supposed to suggest files until suggestFile() returns TRUE, or we + * have no more suggestions. + * @param string $path_fragment + * The key that this plugin was registered with. + * With trailing '/'. + * @param string $path_suffix + * Second part of the canonical path, ending with '.php'. + * @param int|string $id + * Id under which the plugin was registered. + * This may be a numeric id, or a string key. + * + * @return bool|null + * TRUE, if the file was found. + * FALSE, otherwise. + * + * NOTE: + * The signature of this method has changed since the legacy base interface, + * with a new optional parameter being added. + * Due to a bug in PHP 5.3.0 - 5.3.8, redeclaring the method with the + * modified signature would result in a fatal error in these PHP versions. + * This is why the method is commented out. + * The additional optional parameter can still be added in implementations. + */ + # function findFile($api, $path_fragment, $path_suffix, $id = NULL); +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/Psr4FinderPlugin.php b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/Psr4FinderPlugin.php new file mode 100644 index 000000000..8ecd8b7ea --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/Plugin/Psr4FinderPlugin.php @@ -0,0 +1,34 @@ +getClass(), strlen($logical_base_path)); + $relative_path = str_replace('\\', '/', $relative_classname) . '.php'; + return $api->suggestFile($base_dir . '/' . $relative_path); + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassFinder/ProxyClassFinder.php b/frontend/drupal/modules/xautoload/src/ClassFinder/ProxyClassFinder.php new file mode 100644 index 000000000..97ce40681 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassFinder/ProxyClassFinder.php @@ -0,0 +1,90 @@ +finder = $finder; + } + + /** + * {@inheritdoc} + */ + function loadClass($class) { + $this->initFinder(); + $this->finder->loadClass($class); + } + + /** + * {@inheritdoc} + */ + function apiFindFile($api, $class) { + $this->initFinder(); + + return $this->finder->apiFindFile($api, $class); + } + + /** + * @param CacheMissObserverInterface $observer + */ + function observeFirstCacheMiss($observer) { + if (!$this->initialized) { + $this->cacheMissObservers[] = $observer; + } + else { + $observer->cacheMiss($this->finder); + } + } + + /** + * @return ClassFinderInterface + */ + function getFinder() { + $this->initFinder(); + + return $this->finder; + } + + /** + * Initialize the finder and notify cache miss observers. + */ + protected function initFinder() { + if (!$this->initialized) { + $this->initialized = TRUE; + foreach ($this->cacheMissObservers as $operation) { + $operation->cacheMiss($this->finder); + } + } + } + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractCachedClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractCachedClassLoader.php new file mode 100644 index 000000000..a8cd44a43 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractCachedClassLoader.php @@ -0,0 +1,51 @@ +checkRequirements()) { + $class = get_class($loader); + throw new CacheNotSupportedException("Unable to use $class, because the respetive PHP extension is not enabled."); + } + $cacheManager->observeCachePrefix($loader); + + return $loader; + } + + /** + * @return bool + */ + protected abstract function checkRequirements(); + + /** + * {@inheritdoc} + */ + function setCachePrefix($prefix) { + $this->prefix = $prefix; + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoader.php new file mode 100644 index 000000000..f5b7ca828 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoader.php @@ -0,0 +1,41 @@ += 0) { + spl_autoload_register(array($this, 'loadClass'), TRUE, $prepend); + } + elseif ($prepend) { + $loaders = spl_autoload_functions(); + spl_autoload_register(array($this, 'loadClass')); + foreach ($loaders as $loader) { + spl_autoload_unregister($loader); + spl_autoload_register($loader); + } + } + else { + spl_autoload_register(array($this, 'loadClass')); + } + } + + /** + * Unregister from the spl autoload stack. + */ + function unregister() { + spl_autoload_unregister(array($this, 'loadClass')); + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoaderDecorator.php b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoaderDecorator.php new file mode 100644 index 000000000..f37741225 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractClassLoaderDecorator.php @@ -0,0 +1,41 @@ +finder = $finder; + } + + /** + * Replace the finder with another one. + * + * @param ClassFinderInterface $finder + * The object that does the actual class finding. + */ + function setFinder($finder) { + $this->finder = $finder; + } + + /** + * {@inheritdoc} + */ + function loadClass($class) { + $this->finder->loadClass($class); + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractQueuedCachedClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractQueuedCachedClassLoader.php new file mode 100644 index 000000000..004c0a331 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/AbstractQueuedCachedClassLoader.php @@ -0,0 +1,137 @@ +observeCachePrefix($loader); + + return $loader; + } + + /** + * {@inheritdoc} + */ + function loadClass($class) { + + // Look if the cache has anything for this class. + if (isset($this->classFiles[$class])) { + $file = $this->classFiles[$class]; + // The is_file() check may cost around 0.0045 ms per class file, but this + // depends on your system of course. + if (is_file($file)) { + require $file; + + return; + } + $this->toBeDeleted[$class] = $file; + unset($this->classFiles[$class]); + ++$this->n; + } + + // Resolve cache miss. + $api = new LoadClassGetFileInjectedApi($class); + if ($this->finder->apiFindFile($api, $class)) { + // Queue the result for the cache. + $this->toBeAdded[$class] + = $this->classFiles[$class] + = $api->getFile(); + ++$this->n; + } + + // Save the cache if enough has been queued up. + if ($this->n >= $this->nMax) { + $this->classFiles = $this->updateClassFiles($this->toBeAdded, $this->toBeDeleted); + $this->toBeDeleted = array(); + $this->toBeAdded = array(); + $this->nMax *= 2; + $this->n = 0; + } + } + + /** + * Set the new cache prefix after a flush cache. + * + * @param string $prefix + * A prefix for the storage key in APC. + */ + function setCachePrefix($prefix) { + $this->classFiles = $this->loadClassFiles($prefix); + } + + /** + * @param string $prefix + * + * @return string[] + */ + abstract protected function loadClassFiles($prefix); + + /** + * @param string[] $toBeAdded + * @param string[] $toBeRemoved + * + * @return string[] + */ + abstract protected function updateClassFiles($toBeAdded, $toBeRemoved); + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/ApcClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcClassLoader.php new file mode 100644 index 000000000..ebf57b6c4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcClassLoader.php @@ -0,0 +1,39 @@ +prefix . $class)) { + if (is_file($file)) { + require $file; + + return; + } + apc_delete($this->prefix . $class); + } + + // Resolve cache miss. + $api = new LoadClassGetFileInjectedApi($class); + if ($this->finder->apiFindFile($api, $class)) { + apc_store($this->prefix . $class, $api->getFile()); + } + } + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuClassLoader.php new file mode 100644 index 000000000..53753e6f5 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuClassLoader.php @@ -0,0 +1,38 @@ +prefix . $class)) { + if (is_file($file)) { + require $file; + + return; + } + \apcu_delete($this->prefix . $class); + } + + // Resolve cache miss. + $api = new LoadClassGetFileInjectedApi($class); + if ($this->finder->apiFindFile($api, $class)) { + \apcu_store($this->prefix . $class, $api->getFile()); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuQueuedCachedClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuQueuedCachedClassLoader.php new file mode 100644 index 000000000..dc7cf7145 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/ApcuQueuedCachedClassLoader.php @@ -0,0 +1,52 @@ +prefix = $prefix; + $cached = \apcu_fetch($this->prefix); + + return !empty($cached) + ? $cached + : array(); + } + + /** + * @param string[] $toBeAdded + * @param string[] $toBeRemoved + * + * @return string[] + */ + protected function updateClassFiles($toBeAdded, $toBeRemoved) { + + $class_files = $toBeAdded; + // Other requests may have already written to the cache, so we get an up to + // date version. + $cached = \apcu_fetch($this->prefix); + if (!empty($cached)) { + $class_files += $cached; + foreach ($toBeRemoved as $class => $file) { + if (isset($class_files[$class]) && $class_files[$class] === $file) { + unset($class_files[$class]); + } + } + } + + \apcu_store($this->prefix, $class_files); + + return $class_files; + } + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/CacheNotSupportedException.php b/frontend/drupal/modules/xautoload/src/ClassLoader/CacheNotSupportedException.php new file mode 100644 index 000000000..b7623bd42 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/CacheNotSupportedException.php @@ -0,0 +1,5 @@ +cacheName = 'xautoload_db_cache:' . $prefix; + $cached = cache_get($this->cacheName); + return isset($cached->data) + ? $cached->data + : array(); + } + + /** + * @param string[] $toBeAdded + * @param string[] $toBeRemoved + * + * @return string[] + */ + protected function updateClassFiles($toBeAdded, $toBeRemoved) { + + $class_files = $toBeAdded; + // Other requests may have already written to the cache, so we get an up to + // date version. + $cached = cache_get($this->cacheName); + if (isset($cached->data)) { + $class_files += $cached->data; + foreach ($toBeRemoved as $class => $file) { + if (isset($class_files[$class]) && $class_files[$class] === $file) { + unset($class_files[$class]); + } + } + } + + cache_set($this->cacheName, $class_files); + + return $class_files; + } + +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/WinCacheClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/WinCacheClassLoader.php new file mode 100644 index 000000000..ee8376d5d --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/WinCacheClassLoader.php @@ -0,0 +1,39 @@ +prefix . $class)) { + if (is_file($file)) { + require $file; + + return; + } + wincache_ucache_delete($this->prefix . $class); + } + + // Resolve cache miss. + $api = new LoadClassGetFileInjectedApi($class); + if ($this->finder->apiFindFile($api, $class)) { + wincache_ucache_set($this->prefix . $class, $api->getFile()); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/ClassLoader/XCacheClassLoader.php b/frontend/drupal/modules/xautoload/src/ClassLoader/XCacheClassLoader.php new file mode 100644 index 000000000..a61cb9170 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/ClassLoader/XCacheClassLoader.php @@ -0,0 +1,40 @@ +prefix . $class) + && $file = xcache_get($this->prefix . $class) + ) { + if (is_file($file)) { + require $file; + + return; + } + xcache_unset($this->prefix . $class); + } + + // Resolve cache miss. + $api = new LoadClassGetFileInjectedApi($class); + if ($this->finder->apiFindFile($api, $class)) { + xcache_set($this->prefix . $class, $api->getFile()); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/DIC/ServiceContainer.php b/frontend/drupal/modules/xautoload/src/DIC/ServiceContainer.php new file mode 100644 index 000000000..83fd0ebee --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DIC/ServiceContainer.php @@ -0,0 +1,76 @@ +services[$key]) + ? $this->services[$key] + : $this->services[$key] = $this->factory->$key($this) ? : FALSE; + } + + /** + * Unset the service for a specific key. + * + * @param string $key + */ + function reset($key) { + $this->services[$key] = NULL; + } + + /** + * Register a new service under the given key. + * + * @param string $key + * @param mixed $service + */ + function set($key, $service) { + $this->services[$key] = $service; + } + + /** + * Magic getter for a service. + * + * @param string $key + * + * @return mixed + * + * @throws \Exception + */ + function __get($key) { + if (isset($this->services[$key])) { + return $this->services[$key]; + } + if (!method_exists($this->factory, $key)) { + throw new \Exception("Unsupported key '$key' for service factory."); + } + + return $this->services[$key] = $this->factory->$key($this) ? : FALSE; + } + + /** + * @param ServiceFactory $factory + */ + function __construct($factory) { + $this->factory = $factory; + } +} diff --git a/frontend/drupal/modules/xautoload/src/DIC/ServiceContainerInterface.php b/frontend/drupal/modules/xautoload/src/DIC/ServiceContainerInterface.php new file mode 100644 index 000000000..312c4f124 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DIC/ServiceContainerInterface.php @@ -0,0 +1,47 @@ +classFinder + * @property DrupalSystemInterface $system + * @property DrupalPhaseControl $phaseControl + * @property DrupalExtensionAdapter $extensionRegistrationService + * @property ExtensionNamespaces extensionNamespaces + * @property LibrariesInfoAlter librariesInfoAlter + * + * @see \Drupal\xautoload\DIC\ServiceContainer + * @see \Drupal\xautoload\DIC\ServiceFactory + */ +interface ServiceContainerInterface { + + /** + * Retrieves a lazy-instantiated service. + * + * @param string $key + * A key to specify a service. + * @return mixed + * The service for the given key. Usually an object. + */ + function __get($key); +} diff --git a/frontend/drupal/modules/xautoload/src/DIC/ServiceFactory.php b/frontend/drupal/modules/xautoload/src/DIC/ServiceFactory.php new file mode 100644 index 000000000..8c21899b6 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DIC/ServiceFactory.php @@ -0,0 +1,174 @@ +finder, $services->classMapGenerator); + } + + /** + * @param ServiceContainer $services + * + * @return ClassMapGenerator + */ + function classMapGenerator($services) { + return new CachedClassMapGenerator($services->classMapGeneratorRaw, $services->system); + } + + /** + * @param ServiceContainer $services + * + * @return ClassMapGenerator + */ + function classMapGeneratorRaw($services) { + return new ClassMapGenerator(); + } + + /** + * @param ServiceContainer $services + * + * @return DrupalExtensionAdapter + */ + function extensionRegistrationService($services) { + return new DrupalExtensionAdapter($services->system, $services->finder); + } + + /** + * @param ServiceContainer $services + * + * @return CacheManager + */ + function cacheManager($services) { + return CacheManager::create($services->system); + } + + /** + * Proxy class finder. + * + * @param ServiceContainer $services + * + * @return ClassFinderInterface + * Proxy object wrapping the class finder. + * This is used to delay namespace registration until the first time the + * finder is actually used. + * (which might never happen thanks to the APC cache) + */ + function proxyFinder($services) { + // The class finder is cheap to create, so it can use an identity proxy. + return new ProxyClassFinder($services->finder); + } + + /** + * The class finder (alias for 'finder'). + * + * @param ServiceContainer $services + * + * @return ClassFinderInterface + * Object that can find classes, + * and provides methods to register namespaces and prefixes. + * Note: The findClass() method expects an InjectedAPI argument. + */ + function classFinder($services) { + return $services->finder; + } + + /** + * The class finder (alias for 'classFinder'). + * + * @param ServiceContainer $services + * + * @return ClassFinderInterface + * Object that can find classes, + * and provides methods to register namespaces and prefixes. + * Notes: + * - The findClass() method expects an InjectedAPI argument. + * - namespaces are only supported since PHP 5.3 + */ + function finder($services) { + return new ClassFinder(); + } + + /** + * @param ServiceContainer $services + * + * @return DrupalSystemInterface + */ + function system($services) { + return new DrupalSystem(); + } + + /** + * @param ServiceContainer $services + * + * @return DrupalPhaseControl + */ + function phaseControl($services) { + $observers = array( + $services->extensionNamespaces, + new HookXautoload($services->system), + new LibrariesOnInit($services->system), + ); + if ($services->system->variableGet(XAUTOLOAD_VARNAME_REPLACE_CORE, FALSE)) { + $observers[] = new DrupalCoreRegistryRegistrator(); + } + return new DrupalPhaseControl($services->system, $observers); + } + + /** + * @param ServiceContainer $services + * + * @return ExtensionNamespaces + */ + function extensionNamespaces($services) { + return new ExtensionNamespaces($services->system); + } + + /** + * @param ServiceContainer $services + * + * @return LibrariesInfoAlter + */ + function librariesInfoAlter($services) { + return new LibrariesInfoAlter(); + } + +} + diff --git a/frontend/drupal/modules/xautoload/src/DirectoryBehavior/DefaultDirectoryBehavior.php b/frontend/drupal/modules/xautoload/src/DirectoryBehavior/DefaultDirectoryBehavior.php new file mode 100644 index 000000000..4d089b82e --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DirectoryBehavior/DefaultDirectoryBehavior.php @@ -0,0 +1,13 @@ +decorated = $decorated; + $this->system = $system; + } + + /** + * @param string[] $paths + * + * @return string[] + */ + function wildcardPathsToClassmap($paths) { + // Attempt to load from cache. + $cid = 'xautoload:wildcardPathsToClassmap:' . md5(serialize($paths)); + $cache = $this->system->cacheGet($cid); + if ($cache && isset($cache->data)) { + return $cache->data; + } + // Resolve cache miss and save. + $map = $this->decorated->wildcardPathsToClassmap($paths); + $this->system->cacheSet($cid, $map); + + return $map; + } +} diff --git a/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGenerator.php b/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGenerator.php new file mode 100644 index 000000000..213b3e965 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGenerator.php @@ -0,0 +1,47 @@ +wildcardPathsToFiles($paths); + + return $this->filesToClassmap($files); + } + + /** + * @param string[] $files + * + * @return string[] + */ + protected function filesToClassmap($files) { + $map = array(); + foreach ($files as $file) { + $classes = FileInspector::inspectPhpFile($file); + foreach ($classes as $class) { + $map[$class] = $file; + } + } + + return $map; + } + + /** + * @param string[] $paths + * + * @return string[] + */ + protected function wildcardPathsToFiles($paths) { + $wildcardFinder = new WildcardFileFinder(); + $wildcardFinder->addPaths($paths); + + return $wildcardFinder->getFiles(); + } +} \ No newline at end of file diff --git a/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGeneratorInterface.php b/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGeneratorInterface.php new file mode 100644 index 000000000..d3547a399 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/ClassMapGeneratorInterface.php @@ -0,0 +1,14 @@ +dir = $dir; + } + + /** + * @param ClassFinderAdapter $adapter + */ + function writeToAdapter($adapter) { + + // PSR-0 namespaces / prefixes + if (is_file($this->dir . '/autoload_namespaces.php')) { + $prefixes = require $this->dir . '/autoload_namespaces.php'; + if (!empty($prefixes)) { + $adapter->addMultiplePsr0($prefixes); + } + } + + // PSR-4 namespaces + if (is_file($this->dir . '/autoload_psr4.php')) { + $map = require $this->dir . '/autoload_psr4.php'; + if (!empty($map)) { + $adapter->addMultiplePsr4($map); + } + } + + // Class map + if (is_file($this->dir . '/autoload_classmap.php')) { + $class_map = require $this->dir . '/autoload_classmap.php'; + if (!empty($class_map)) { + $adapter->addClassMap($class_map); + } + } + + // Include path + if (is_file($this->dir . '/include_paths.php')) { + $include_paths = require $this->dir . '/include_paths.php'; + if (!empty($include_paths)) { + array_push($include_paths, get_include_path()); + set_include_path(join(PATH_SEPARATOR, $include_paths)); + } + } + + // Include files + if (is_file($this->dir . '/autoload_files.php')) { + $include_files = require $this->dir . '/autoload_files.php'; + foreach ($include_files as $file) { + require $file; + } + } + } +} \ No newline at end of file diff --git a/frontend/drupal/modules/xautoload/src/Discovery/ComposerJson.php b/frontend/drupal/modules/xautoload/src/Discovery/ComposerJson.php new file mode 100644 index 000000000..a95f722d3 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/ComposerJson.php @@ -0,0 +1,132 @@ +data = $data; + $this->pathPrefix = $path_prefix; + } + + /** + * @param ClassFinderAdapter $adapter + */ + function writeToAdapter(ClassFinderAdapter $adapter) { + + $data = $this->data; + + if (!empty($data['include-path'])) { + $this->addIncludePaths((array)$data['include-path']); + } + + if (!empty($data['autoload']['psr-0'])) { + $map = $this->transformMultiple($data['autoload']['psr-0']); + $adapter->addMultiplePsr0($map); + } + + if (!empty($data['autoload']['psr-4'])) { + $map = $this->transformMultiple($data['autoload']['psr-4']); + $adapter->addMultiplePsr4($map); + } + + if (!empty($data['autoload']['classmap'])) { + $this->addClassmapSources($adapter, (array)$data['autoload']['classmap']); + } + + if (!empty($data['autoload']['files'])) { + foreach ($data['autoload']['files'] as $file) { + require $this->pathPrefix . $file; + } + } + } + + /** + * @param array $multiple + * + * @return array[] + */ + protected function transformMultiple(array $multiple) { + foreach ($multiple as &$paths) { + $paths = (array)$paths; + foreach ($paths as &$path) { + if ('' === $path || '/' !== $path[0]) { + $path = $this->pathPrefix . $path; + } + } + } + return $multiple; + } + + /** + * @param string[] $include_paths + */ + protected function addIncludePaths(array $include_paths) { + foreach ($include_paths as &$path) { + $path = $this->pathPrefix . $path; + } + array_push($include_paths, get_include_path()); + set_include_path(join(PATH_SEPARATOR, $include_paths)); + } + + /** + * @param ClassFinderAdapter $adapter + * @param string[] $sources_raw + * Array of files and folders to scan for class implementations. + */ + protected function addClassmapSources($adapter, array $sources_raw) { + foreach ($sources_raw as &$path) { + $path = $this->pathPrefix . $path; + } + $adapter->addClassmapSources($sources_raw); + } +} diff --git a/frontend/drupal/modules/xautoload/src/Discovery/ComposerJsonTargetDir.php b/frontend/drupal/modules/xautoload/src/Discovery/ComposerJsonTargetDir.php new file mode 100644 index 000000000..26a452807 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/ComposerJsonTargetDir.php @@ -0,0 +1,118 @@ +targetDir = rtrim($data['target-dir'], '/') . '/'; + } + + /** + * @param ClassFinderAdapter $adapter + * + * @throws \Exception + */ + function writeToAdapter(ClassFinderAdapter $adapter) { + + $data = $this->data; + + if (!empty($data['include-path'])) { + $paths = $this->pathsResolveTargetDir((array) $data['include-path']); + $this->addIncludePaths($paths, $this->pathPrefix); + } + + if (!empty($data['autoload']['psr-0'])) { + $this->addMultipleWithTargetDir($adapter, $data['autoload']['psr-0']); + } + + if (!empty($data['autoload']['psr-4'])) { + throw new \Exception("PSR-4 is incompatible with target-dir."); + } + + if (!empty($data['autoload']['classmap'])) { + $paths = $this->pathsResolveTargetDir($data['autoload']['classmap']); + $this->addClassmapSources($adapter, $paths); + } + + if (!empty($data['autoload']['files'])) { + $paths = $this->pathsResolveTargetDir($data['autoload']['files']); + foreach ($paths as $file) { + require $this->pathPrefix . $file; + } + } + } + + /** + * @param string[] $paths + * + * @return string[] + */ + protected function pathsResolveTargetDir(array $paths) { + $strlen = strlen($this->targetDir); + foreach ($paths as &$path) { + if (0 === strpos($path, $this->targetDir)) { + $path = substr($path, $strlen); + } + } + + return $paths; + } + + /** + * @param ClassFinderAdapter $adapter + * @param array $prefixes + */ + protected function addMultipleWithTargetDir(ClassFinderAdapter $adapter, array $prefixes) { + $default_behavior = new DefaultDirectoryBehavior(); + $psr0_behavior = new Psr0DirectoryBehavior(); + $namespace_map = array(); + $prefix_map = array(); + $target_dir_strlen = strlen($this->targetDir); + foreach ($prefixes as $prefix => $paths) { + if (FALSE === strpos($prefix, '\\')) { + $logical_base_path = Util::prefixLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + if (0 !== strpos($deep_path, $this->targetDir)) { + continue; + } + $deep_path = $this->pathPrefix . substr($deep_path, $target_dir_strlen); + $prefix_map[$logical_base_path][$deep_path] = $default_behavior; + } + } + $logical_base_path = Util::namespaceLogicalPath($prefix); + foreach ((array) $paths as $root_path) { + $deep_path = strlen($root_path) + ? rtrim($root_path, '/') . '/' . $logical_base_path + : $logical_base_path; + if (0 !== strpos($deep_path, $this->targetDir)) { + continue; + } + $deep_path = $this->pathPrefix . substr($deep_path, $target_dir_strlen); + $namespace_map[$logical_base_path][$deep_path] = $psr0_behavior; + } + } + if (!empty($prefix_map)) { + $adapter->getPrefixMap()->registerDeepPaths($prefix_map); + } + $adapter->getNamespaceMap()->registerDeepPaths($namespace_map); + } +} diff --git a/frontend/drupal/modules/xautoload/src/Discovery/FileInspector.php b/frontend/drupal/modules/xautoload/src/Discovery/FileInspector.php new file mode 100644 index 000000000..f2df02589 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/FileInspector.php @@ -0,0 +1,98 @@ +getMessage(), + // The Exception code. Defaults to 0. + 0, + // The previous exception used for exception chaining. + $e); + } + + return self::inspectFileContents($contents); + } + + /** + * @param string $contents + * The PHP file contents obtained with php_strip_whitespace($path). + * + * @return string[] + * Classes discovered in the file. + */ + protected static function inspectFileContents($contents) { + $traits = version_compare(PHP_VERSION, '5.4', '<') + ? '' + : '|trait'; + + // return early if there is no chance of matching anything in this file + if (!preg_match('{\b(?:class|interface' . $traits . ')\s}i', $contents)) { + return array(); + } + + // strip heredocs/nowdocs + $contents = preg_replace( + '{<<<\'?(\w+)\'?(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\1(?=\r\n|\n|\r|;)}s', + 'null', + $contents); + + // strip strings + $contents = preg_replace( + '{"[^"\\\\]*(\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(\\\\.[^\'\\\\]*)*\'}s', + 'null', + $contents); + + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (FALSE !== $pos && FALSE === strpos(substr($contents, $pos), '])(?Pclass|interface' . $traits . ') \s+ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*) + | \b(?])(?Pnamespace) (?P\s+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\s*\\\\\s*[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)? \s*[\{;] + ) + }ix', + $contents, + $matches + ); + + $classes = array(); + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) + . '\\'; + } + else { + $classes[] = ltrim($namespace . $matches['name'][$i], '\\'); + } + } + + return $classes; + } +} diff --git a/frontend/drupal/modules/xautoload/src/Discovery/WildcardFileFinder.php b/frontend/drupal/modules/xautoload/src/Discovery/WildcardFileFinder.php new file mode 100644 index 000000000..436386824 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Discovery/WildcardFileFinder.php @@ -0,0 +1,211 @@ + $value) { + if (1 + && FALSE !== strpos($path, '*') + && preg_match('#^([^\*]*)/(.*\*.*)$#', $path, $m) + ) { + // Resolve wildcards. + $this->value = $value; + list(, $base, $wildcard) = $m; + $this->scanDirectory($base, $wildcard); + } + else { + // Register the file directly. + $this->files[$path] = $value; + } + } + } + + /** + * @param string[] $paths + * Array keys are file paths or wildcard file paths. + * @param mixed $value + */ + function addPaths(array $paths, $value = TRUE) { + foreach ($paths as $path) { + if (1 + && FALSE !== strpos($path, '*') + && preg_match('#^([^\*]*)/(.*\*.*)$#', $path, $m) + ) { + // Resolve wildcards. + $this->value = $value; + list(, $base, $wildcard) = $m; + $this->scanDirectory($base, $wildcard); + } + elseif (is_dir($path)) { + // Resolve wildcards. + $this->value = $value; + $this->scanDirectory($path . '/', '**/*.inc'); + $this->scanDirectory($path . '/', '**/*.php'); + } + elseif (is_file($path)) { + // Register the file directly. + $this->files[$path] = $value; + } + } + } + + /** + * @return string[] + */ + function getFiles() { + return array_keys($this->files); + } + + /** + * @return mixed[] + */ + function getDrupalFiles() { + return $this->files; + } + + /** + * @param string $dir + * Base folder, e.g. "sites/all/modules/foo/includes", which does NOT + * contain any asterisk ("*"). + * @param string $wildcard + * Suffix which may contain asterisks. + */ + protected function scanDirectory($dir, $wildcard) { + if (!is_dir($dir)) { + return; + } + if (FALSE === strpos($wildcard, '*')) { + // $wildcard is a fixed string, not a wildcard. + $this->suggestFile($dir . '/' . $wildcard); + } + elseif ('**' === $wildcard) { + // Trick: "$a/**" == union of "$a/*" and "$a/*/**" + $this->scanDirectoryLevel($dir, '*'); + $this->scanDirectoryLevel($dir, '*', '**'); + } + elseif ('**/' === substr($wildcard, 0, 3)) { + // Trick: "$a/**/$b" == union of "$a/$b" and "$a/*/**/$b" + $remaining = substr($wildcard, 3); + $this->scanDirectory($dir, $remaining); + $this->scanDirectoryLevel($dir, '*', $wildcard); + } + elseif (FALSE !== ($slashpos = strpos($wildcard, '/'))) { + // $wildcard consists of more than one fragment. + $fragment = substr($wildcard, 0, $slashpos); + $remaining = substr($wildcard, $slashpos + 1); + if (FALSE === strpos($fragment, '*')) { + $this->scanDirectory($dir . '/' . $fragment, $remaining); + } + else { + $this->scanDirectoryLevel($dir, $fragment, $remaining); + } + } + else { + // $wildcard represents a file name. + $this->scanDirectoryLevel($dir, $wildcard); + } + } + + /** + * @param string $dir + * Base directory, not containing any wildcard. + * @param string $fragment + * Wildcard path fragment to be processed now. This is never '**', but it + * always contains at least one asterisk. + * @param null $remaining + * Optional rest of the wildcard string, that may contain path fragments to + * be processed later. + * + * @throws \Exception + */ + protected function scanDirectoryLevel($dir, $fragment, $remaining = NULL) { + + if (!is_dir($dir)) { + return; + } + + if ('**' === $fragment) { + throw new \Exception("Fragment must not be '**'."); + } + + foreach (scandir($dir) as $candidate) { + if (!$this->validateCandidate($candidate, $fragment)) { + continue; + } + + if (!isset($remaining)) { + $this->suggestFile($dir . '/' . $candidate); + } + else { + $this->scanDirectory($dir . '/' . $candidate, $remaining); + } + } + } + + /** + * @param $candidate + * String to be checked against the wildcard. + * @param $wildcard + * Wildcard string like '*', '*.*' or '*.inc'. + * + * @return bool + * TRUE, if $candidate matches $wildcard. + */ + protected function validateCandidate($candidate, $wildcard) { + + if ($candidate == '.' || $candidate == '..') { + return FALSE; + } + if (strpos($candidate, '*') !== FALSE) { + return FALSE; + } + if ($wildcard == '*' || $wildcard == '**') { + return TRUE; + } + + // More complex wildcard string. + $fragments = array(); + foreach (explode('*', $wildcard) as $fragment) { + $fragments[] = preg_quote($fragment); + } + $regex = implode('.*', $fragments); + + return preg_match("/^$regex$/", $candidate); + } + + /** + * @param string $path + * Add a new file path to $this->filesInRegistry(). + */ + protected function suggestFile($path) { + if (is_file($path)) { + $this->files[$path] = $this->value; + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystem.php b/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystem.php new file mode 100644 index 000000000..9b8ea60a8 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystem.php @@ -0,0 +1,178 @@ +condition('name', $extension_names); + $q->fields('system', array('name', 'type')); + + return $q->execute()->fetchAllKeyed(); + } + + /** + * {@inheritdoc} + */ + function getActiveExtensions() { + try { + // Doing this directly tends to be a lot faster than system_list(). + return db_query("SELECT name, type from {system} WHERE status = 1") + ->fetchAllKeyed(); + } + catch (\DatabaseConnectionNotDefinedException $e) { + // During install, the database is not available. + // At this time only the system module is "installed". + /** See https://www.drupal.org/node/2393205 */ + return array('system' => 'module'); + } + catch (\PDOException $e) { + // Some time later during install, there is a database but not yet a system table. + // At this time only the system module is "installed". + // @todo Check if this is really a "Table 'system' doesn't exist'" exception. + return array('system' => 'module'); + } + } + + /** + * {@inheritdoc} + */ + function moduleImplements($hook) { + return module_implements($hook); + } + + /** + * Wrapper for module_list() + * + * @return array + */ + function moduleList() { + return module_list(); + } + + /** + * @see libraries_info() + * + * @throws \Exception + * @return mixed + */ + function getLibrariesInfo() { + if (!function_exists('libraries_info')) { + // Libraries is at a lower version, which does not have this function. + return array(); + } + # drupal_static_reset('libraries_info'); + return libraries_info(); + } + + /** + * @see libraries_get_path() + * + * @param string $name + * Name of the library. + * + * @throws \Exception + * @return string|false + */ + function librariesGetPath($name) { + if (!function_exists('libraries_get_path')) { + throw new \Exception('Function libraries_get_path() does not exist.'); + } + return libraries_get_path($name); + } + + /** + * Called from xautoload_install() to set the module weight. + * + * @param int $weight + * New module weight for xautoload. + */ + public function installSetModuleWeight($weight) { + db_update('system') + ->fields(array('weight' => $weight)) + ->condition('name', 'xautoload') + ->condition('type', 'module') + ->execute(); + system_list_reset(); + } + + /** + * @param string $cid + * @param string $bin + * + * @return mixed + * + * @see cache_get() + */ + public function cacheGet($cid, $bin = 'cache') { + return cache_get($cid, $bin); + } + + /** + * @param string $cid + * @param mixed $data + * @param string $bin + * + * @return mixed + * + * @see cache_set() + */ + public function cacheSet($cid, $data, $bin = 'cache') { + cache_set($cid, $data, $bin); + } + + /** + * @param string|null $cid + * @param string|null $bin + * + * @see cache_clear_all() + */ + public function cacheClearAll($cid = NULL, $bin = NULL) { + cache_clear_all($cid, $bin); + } + + /** + * @param string $key + */ + public function drupalStaticReset($key) { + \drupal_static_reset($key); + } +} diff --git a/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystemInterface.php b/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystemInterface.php new file mode 100644 index 000000000..6a76143a3 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/DrupalSystem/DrupalSystemInterface.php @@ -0,0 +1,145 @@ +finder = $finder; + $this->system = $system; + } + + /** + * Find the file for a class that in PSR-0 or PEAR would be in + * $psr_0_root . '/' . $path_fragment . $path_suffix + * + * @param InjectedApiInterface $api + * @param string $logical_base_path + * @param string $relative_path + * + * @return bool|null + * TRUE, if the file was found. + * FALSE or NULL, otherwise. + */ + function findFile($api, $logical_base_path, $relative_path) { + + // Prevent recursion if this is called from libraries_info(). + // @todo Find a better way to do this? + $backtrace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') + ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) + : debug_backtrace(FALSE); + foreach ($backtrace as $call) { + if ('libraries_info' === $call['function']) { + return FALSE; + } + } + + $this->finder->getNamespaceMap()->unregisterDeepPath('', ''); + $this->finder->getPrefixMap()->unregisterDeepPath('', ''); + $this->registerAllLibraries(); + return $this->finder->apiFindFile($api, $api->getClass()); + } + + /** + * Registers all libraries that have an "xautoload" setting. + */ + private function registerAllLibraries() { + $adapter = \xautoload_InjectedAPI_hookXautoload::create($this->finder, ''); + foreach ($info = $this->getLibrariesXautoloadInfo() as $name => $pathAndCallback) { + list($path, $callback) = $pathAndCallback; + if (!is_callable($callback)) { + continue; + } + if (!is_dir($path)) { + continue; + } + $adapter->setExtensionDir($path); + call_user_func($callback, $adapter, $path); + } + } + + /** + * @return array[] + */ + private function getLibrariesXautoloadInfo() { + $cached = $this->system->cacheGet(XAUTOLOAD_CACHENAME_LIBRARIES_INFO); + if (FALSE !== $cached) { + return $cached->data; + } + $info = $this->buildLibrariesXautoloadInfo(); + $this->system->cacheSet(XAUTOLOAD_CACHENAME_LIBRARIES_INFO, $info); + return $info; + } + + /** + * @return array[] + */ + private function buildLibrariesXautoloadInfo() { + // @todo Reset drupal_static('libraries') ? + $all = array(); + foreach ($this->system->getLibrariesInfo() as $name => $info) { + if (!isset($info['xautoload'])) { + continue; + } + $callback = $info['xautoload']; + if (!is_callable($callback)) { + continue; + } + /** See https://www.drupal.org/node/2473901 */ + $path = isset($info['library path']) + ? $info['library path'] + : $this->system->librariesGetPath($name); + if (FALSE === $path) { + continue; + } + $all[$name] = array($path, $callback); + } + return $all; + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Libraries/LibrariesInfoAlter.php b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesInfoAlter.php new file mode 100644 index 000000000..dc3340adc --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesInfoAlter.php @@ -0,0 +1,55 @@ + &$library_info) { + if (1 + && isset($library_info['xautoload']) + && is_callable($library_info['xautoload']) + ) { + $this->alterLibraryInfo($library_info, $library_name); + } + } + } + + /** + * @param array $library_info + * @param string $library_name + */ + private function alterLibraryInfo(&$library_info, $library_name) { + $callable = $library_info['xautoload']; + if ($callable instanceof \Closure) { + // Wrap the closure so it can be serialized. + $callable = new SerializableClosureWrapper( + $library_info['xautoload'], + // The module name and library name allow the closure to be recovered on + // unserialize. + $library_info['module'], + $library_name); + $library_info['xautoload'] = $callable; + } + # $library_info['callbacks']['pre-load'][] = new LibrariesPreLoadCallback($callable); + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Libraries/LibrariesOnInit.php b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesOnInit.php new file mode 100644 index 000000000..a2c3dce1b --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesOnInit.php @@ -0,0 +1,91 @@ +system = $system; + } + + /** + * Wake up after a cache fail. + * + * @param ExtendedClassFinderInterface $finder + * @param string[] $extensions + * Extension type by extension name. + */ + public function wakeUp(ExtendedClassFinderInterface $finder, array $extensions) { + $this->finder = $finder; + } + + /** + * Enter the boot phase of the request, where all bootstrap module files are included. + */ + public function enterBootPhase() { + // Nothing. + } + + /** + * Enter the main phase of the request, where all module files are included. + */ + public function enterMainPhase() { + $this->registerLibrariesFinderPlugin(); + } + + /** + * React to new extensions that were just enabled. + * + * @param string $name + * @param string $type + */ + public function welcomeNewExtension($name, $type) { + // Nothing. + } + + /** + * React to xautoload_modules_enabled() + * + * @param string[] $modules + * New module names. + */ + public function modulesEnabled($modules) { + $this->system->drupalStaticReset('libraries_info'); + $this->system->cacheClearAll(XAUTOLOAD_CACHENAME_LIBRARIES_INFO, 'cache'); + $this->registerLibrariesFinderPlugin(); + } + + /** + * Registers all libraries that have an "xautoload" setting. + */ + private function registerLibrariesFinderPlugin() { + $plugin = new LibrariesFinderPlugin($this->finder, $this->system); + $this->finder->getPrefixMap()->registerDeepPath('', '', $plugin); + $this->finder->getNamespaceMap()->registerDeepPath('', '', $plugin); + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Libraries/LibrariesPreLoadCallback.php b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesPreLoadCallback.php new file mode 100644 index 000000000..8048bfd60 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Libraries/LibrariesPreLoadCallback.php @@ -0,0 +1,51 @@ +callable = $callable; + } + + /** + * Callback that is applied directly before the library is loaded. At this + * point the library contains variant-specific information, if specified. Note + * that in this group the 'variants' property is no longer available. + * + * @param array $library + * An array of library information belonging to the top-level library, a + * specific version, a specific variant or a specific variant of a specific + * version. Because library information such as the 'files' property (see + * above) can be declared in all these different locations of the library + * array, but a callback may have to act on all these different parts of the + * library, it is called recursively for each library with a certain part of + * the libraries array passed as $library each time. + * @param string|null $version + * If the $library array belongs to a certain version (see above), a string + * containing the version. This argument may be empty, so NULL should be + * specified as the default value. + * @param string|null $variant + * If the $library array belongs to a certain variant (see above), a string + * containing the variant name. This argument may be empty, so NULL should + * be specified as the default value. + */ + function __invoke($library, $version, $variant) { + if (!empty($library['installed'])) { + xautoload()->proxyFinder->observeFirstCacheMiss( + new LibraryCacheMissObserver($this->callable, $library['library path'])); + } + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Libraries/LibraryCacheMissObserver.php b/frontend/drupal/modules/xautoload/src/Libraries/LibraryCacheMissObserver.php new file mode 100644 index 000000000..bda0df719 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Libraries/LibraryCacheMissObserver.php @@ -0,0 +1,45 @@ +callable = $callable; + $this->path = $path; + } + + /** + * Executes the operation. + * + * This method will only be called if and when the "real" class finder is + * initialized. + * + * @param ExtendedClassFinderInterface $finder + * The class finder. + */ + function cacheMiss($finder) { + $adapter = \xautoload_InjectedAPI_hookXautoload::create($finder, $this->path); + call_user_func($this->callable, $adapter, $this->path); + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Libraries/SerializableClosureWrapper.php b/frontend/drupal/modules/xautoload/src/Libraries/SerializableClosureWrapper.php new file mode 100644 index 000000000..90f8982b9 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Libraries/SerializableClosureWrapper.php @@ -0,0 +1,85 @@ +closure = $closure; + $this->moduleName = $moduleName; + $this->libraryName = $libraryName; + } + + public function __sleep() { + return array('moduleName', 'libraryName'); + } + + /** + * @param \Drupal\xautoload\Adapter\LocalDirectoryAdapter $adapter + */ + public function __invoke($adapter) { + $closure = $this->lazyGetClosure(); + if ($closure instanceof \Closure) { + $closure($adapter); + } + } + + /** + * @return \Closure|FALSE + */ + private function lazyGetClosure() { + return isset($this->closure) + ? $this->closure + : $this->closure = $this->loadClosure(); + } + + /** + * @return \Closure|FALSE + */ + private function loadClosure() { + $source_function = $this->moduleName . '_libraries_info'; + if (!function_exists($source_function)) { + return FALSE; + } + $module_libraries = $source_function(); + if (!isset($module_libraries[$this->libraryName]['xautoload'])) { + return FALSE; + } + $closure_candidate = $module_libraries[$this->libraryName]['xautoload']; + if (!$closure_candidate instanceof \Closure) { + return FALSE; + } + return $closure_candidate; + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Main.php b/frontend/drupal/modules/xautoload/src/Main.php new file mode 100644 index 000000000..7d9204a08 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Main.php @@ -0,0 +1,110 @@ +services = $services; + } + + /** + * @return ServiceContainer + */ + function getServiceContainer() { + return $this->services; + } + + /** + * Invalidate all values stored in APC. + */ + function flushCache() { + $this->services->cacheManager->renewCachePrefix(); + } + + /** + * Register a module in early bootstrap, or from modulename.install. + * + * This is only needed for modules that need autoloading very early in the + * request, or e.g. during uninstall, or any situation that xautoload cannot + * catch up with. + * + * The method will register all autoloading schemes for this module that are + * supported by default: + * - PSR-0: "Drupal\\$module\\Foo" => "$module_dir/lib/Drupal/$module/Foo.php" + * - PEAR-FLAT: $module . "_Foo_Bar" => "$module_dir/lib/Foo/Bar.php" + * + * It will not register anything for PSR-4, since it is not clear whether this + * will be in "/lib/" or "/src/" or elsewhere. + * + * Suggested usage: (in your $modulename.module, or $modulename.install): + * + * xautoload()->registerModule(__FILE__); + * + * @param string $__FILE__ + * File path to a *.module or *.install file. + * The basename of the file MUST be the module name. + * It is recommended to call this from the respective file itself, using the + * __FILE__ constant for this argument. + */ + function registerModule($__FILE__) { + $this->services->extensionNamespaces->registerExtension($__FILE__); + } + + /** + * Register a module as PSR-4, in early bootstrap or from modulename.install + * + * This can be used while Drupal 8 is still undecided whether PSR-4 class + * files should live in "lib" or in "src" by default. + * + * Suggested usage: (in your $modulename.module, or $modulename.install): + * + * // E.g. "Drupal\\$module\\Foo" => "$module_dir/lib/Foo.php" + * xautoload()->registerModulePsr4(__FILE__, 'lib'); + * + * or + * + * // E.g. "Drupal\\$module\\Foo" => "$module_dir/src/Foo.php" + * xautoload()->registerModulePsr4(__FILE__, 'src'); + * + * or + * + * // E.g. "Drupal\\$module\\Foo" => "$module_dir/psr4/Foo.php" + * xautoload()->registerModulePsr4(__FILE__, 'psr4'); + * + * @param string $__FILE__ + * File path to a *.module or *.install file. + * The basename of the file MUST be the module name. + * It is recommended to call this from the respective file itself, using the + * __FILE__ constant for this argument. + * @param string $subdir + * The PSR-4 base directory for the module namespace, relative to the module + * directory. E.g. "src" or "lib". + */ + function registerModulePsr4($__FILE__, $subdir) { + $this->services->extensionNamespaces->registerExtensionPsr4($__FILE__, $subdir); + } + + /** + * Magic getter for service objects. This lets this class act as a proxy for + * the service container. + * + * @param string $key + * @return mixed + */ + function __get($key) { + return $this->services->__get($key); + } +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/DrupalCoreRegistryRegistrator.php b/frontend/drupal/modules/xautoload/src/Phases/DrupalCoreRegistryRegistrator.php new file mode 100644 index 000000000..73200f5a1 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/DrupalCoreRegistryRegistrator.php @@ -0,0 +1,57 @@ +getNamespaceMap()->registerDeepPath('', 'registry', $plugin); + $finder->getPrefixMap()->registerDeepPath('', 'registry', $plugin); + } + + /** + * Enter the boot phase of the request, where all bootstrap module files are included. + */ + public function enterBootPhase() { + // Nothing. + } + + /** + * Enter the main phase of the request, where all module files are included. + */ + public function enterMainPhase() { + // Nothing. + } + + /** + * React to new extensions that were just enabled. + * + * @param string $name + * @param string $type + */ + public function welcomeNewExtension($name, $type) { + // Nothing. + } + + /** + * React to xautoload_modules_enabled() + * + * @param string[] $modules + * New module names. + */ + public function modulesEnabled($modules) { + // Nothing. + } +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/DrupalPhaseControl.php b/frontend/drupal/modules/xautoload/src/Phases/DrupalPhaseControl.php new file mode 100644 index 000000000..40bc209fa --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/DrupalPhaseControl.php @@ -0,0 +1,171 @@ +system = $system; + $this->observers = $observers; + } + + /** + * {@inheritdoc} + */ + public function cacheMiss($finder) { + $this->extensions = $this->system->getActiveExtensions(); + foreach ($this->observers as $observer) { + $observer->wakeUp($finder, $this->extensions); + } + $this->awake = TRUE; + if ($this->bootPhase) { + // We slipped into boot phase while asleep. Need to catch up. + foreach ($this->observers as $observer) { + $observer->enterBootPhase(); + } + } + if ($this->mainPhase) { + // We slipped into main phase while asleep. Need to catch up. + foreach ($this->observers as $observer) { + $observer->enterMainPhase(); + } + } + } + + public function enterBootPhase() { + if ($this->bootPhase) { + // We are already in the main phase. Nothing changes. + return; + } + $this->bootPhase = TRUE; + if (!$this->awake) { + // The entire thing is not initialized yet. + // Postpone until operateOnFinder() + return; + } + foreach ($this->observers as $observer) { + $observer->enterBootPhase(); + } + } + + /** + * Initiate the main phase. + * + * Called from + * @see xautoload_custom_theme() + * @see xautolaod_init() + */ + public function enterMainPhase() { + // Main phase implies boot phase. + $this->enterBootPhase(); + if ($this->mainPhase) { + // We are already in the main phase. Nothing changes. + return; + } + $this->mainPhase = TRUE; + if (!$this->awake) { + // The entire thing is not initialized yet. + // Postpone until operateOnFinder() + return; + } + foreach ($this->observers as $observer) { + $observer->enterMainPhase(); + } + } + + /** + * Checks if new extensions have been enabled, and registers them. + * + * This is called from xautoload_module_implements_alter(), which is called + * whenever a new module is enabled, but also some calls we need to ignore. + */ + public function checkNewExtensions() { + if (!$this->awake) { + // The entire thing is not initialized yet. + // Postpone until operateOnFinder() + return; + } + $activeExtensions = $this->system->getActiveExtensions(); + if ($activeExtensions === $this->extensions) { + // Nothing actually changed. False alarm. + return; + } + // Now check all extensions to find out if any of them is new. + foreach ($activeExtensions as $name => $type) { + if (!isset($this->extensions[$name])) { + // This extension was freshly enabled. + if ('xautoload' === $name) { + // If xautoload is enabled in this request, then boot phase and main + // phase are not properly initialized yet. + $this->enterMainPhase(); + } + // Notify observers about the new extension. + foreach ($this->observers as $observer) { + $observer->welcomeNewExtension($name, $type); + } + } + } + } + + /** + * Called from @see xautoload_modules_enabled() + * + * @param $modules + */ + public function modulesEnabled($modules) { + if (!$this->awake) { + // No need to postpone. + // initMainPhase() will have these modules included. + return; + } + foreach ($this->observers as $observer) { + $observer->modulesEnabled($modules); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/ExtensionNamespaces.php b/frontend/drupal/modules/xautoload/src/Phases/ExtensionNamespaces.php new file mode 100644 index 000000000..a9a4a1894 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/ExtensionNamespaces.php @@ -0,0 +1,253 @@ +system = $system; + } + + /** + * Registers the namespaces for a module even though it might be currently + * disabled, or even though it might be early in the request. + * + * @param string $__FILE__ + */ + public function registerExtension($__FILE__) { + if (NULL === $this->finder) { + // Sleeping.. + $this->queue[$__FILE__] = FALSE; + + return; + } + + $info = pathinfo($__FILE__); + $name = $info['filename']; + + // Check if something was registered before. + if (isset($this->registered[$name])) { + // Already registered. + return; + } + + $this->_registerExtension($name, $info['dirname']); + } + + /** + * Registers the namespace with PSR-4 for a module even though it might be + * currently disabled, or even though it might be early in the request. + * + * @param string $__FILE__ + * @param string $subdir + */ + public function registerExtensionPsr4($__FILE__, $subdir) { + if (NULL === $this->finder) { + // Sleeping.. + $this->queue[$__FILE__] = $subdir; + + return; + } + + $info = pathinfo($__FILE__); + $name = $info['filename']; + + // Check if something was registered before. + if (isset($this->registered[$name])) { + if ('psr-4' === $this->registered[$name]) { + // It already happened. + return; + } + // Unregister the lazy plugins. + $this->finder->getNamespaceMap()->unregisterDeepPath( + 'Drupal/' . $name . '/', + $name + ); + $this->finder->getPrefixMap()->unregisterDeepPath( + str_replace('_', '/', $name) . '/', + $name + ); + } + $this->_registerExtensionPsr4($name, $info['dirname'], $subdir); + } + + /** + * Wake up after a cache fail. + * + * @param ExtendedClassFinderInterface $finder + * @param string[] $extensions + * Extension type by extension name. + */ + public function wakeUp(ExtendedClassFinderInterface $finder, array $extensions) { + $this->finder = $finder; + + // Register queued extensions. + foreach ($this->queue as $__FILE__ => $subdir) { + $info = pathinfo($__FILE__); + $name = $info['filename']; + $dir = $info['dirname']; + if (FALSE === $subdir) { + // This is not PSR-4. + $this->_registerExtension($name, $dir); + } + else { + // This is PSR-4. + $this->_registerExtensionPsr4($name, $dir, $subdir); + } + } + + $extensions = array_diff_key($extensions, $this->registered); + + // Register remaining extensions, using the lazy plugins. + $this->_registerLazyExtensionPlugins($extensions); + } + + /** + * Enter the boot phase of the request, where all bootstrap module files are included. + */ + public function enterBootPhase() { + // Nothing. + } + + /** + * Enter the main phase of the request, where all module files are included. + */ + public function enterMainPhase() { + // Nothing. + } + + /** + * React to new extensions that were just enabled. + * + * @param string $name + * @param string $type + */ + public function welcomeNewExtension($name, $type) { + if (isset($this->registered[$name])) { + // Already registered. + return; + } + $dir = $this->system->drupalGetPath($type, $name); + $this->_registerExtension($name, $dir); + } + + /** + * React to xautoload_modules_enabled() + * + * @param string[] $modules + * New module names. + */ + public function modulesEnabled($modules) { + // Nothing. + } + + /** + * @param string $name + * @param string $dir + */ + private function _registerExtension($name, $dir) { + if (is_dir($lib = $dir . '/lib')) { + $this->finder->addPsr0('Drupal\\' . $name . '\\', $lib); + $this->finder->addPearFlat($name . '_', $lib); + } + if (is_dir($src = $dir . '/src')) { + $this->finder->addPsr4('Drupal\\' . $name . '\\', $src); + } + + $this->registered[$name] = TRUE; + } + + /** + * @param string $name + * @param string $dir + * @param string $subdir + */ + private function _registerExtensionPsr4($name, $dir, $subdir) { + $this->finder->addPsr4('Drupal\\' . $name . '\\', $dir . '/' . $subdir); + + // Re-add the PSR-0 test directory, for consistency's sake. + if (is_dir($lib_tests = $dir . '/lib/Drupal/' . $name . '/Tests')) { + $this->finder->addPsr0('Drupal\\' . $name . '\\Tests\\', $lib_tests); + } + $this->registered[$name] = 'psr-4'; + } + + /** + * Register lazy plugins for enabled Drupal modules and themes, assuming that + * we don't know yet whether they use PSR-0, PEAR-Flat, or none of these. + * + * @param string[] $extensions + * An array where the keys are extension names, and the values are extension + * types like 'module' or 'theme'. + */ + private function _registerLazyExtensionPlugins(array $extensions) { + + $namespaceBehaviors = array(); + $prefixBehaviors = array(); + foreach (array('module', 'theme') as $extension_type) { + $namespaceBehaviors[$extension_type] = new DrupalExtensionNamespaceFinderPlugin( + $extension_type, + $this->finder->getNamespaceMap(), + $this->finder->getPrefixMap(), + $this->system); + $prefixBehaviors[$extension_type] = new DrupalExtensionUnderscoreFinderPlugin( + $extension_type, + $this->finder->getNamespaceMap(), + $this->finder->getPrefixMap(), + $this->system); + } + + $prefix_map = array(); + $namespace_map = array(); + foreach ($extensions as $name => $type) { + if (empty($namespaceBehaviors[$type])) { + // Unsupported extension type, e.g. "theme_engine". + // This can happen if a site was upgraded from Drupal 6. + // See https://drupal.org/comment/8503979#comment-8503979 + continue; + } + if (!empty($this->registered[$name])) { + // The extension has already been processed. + continue; + } + $namespace_map['Drupal/' . $name . '/'][$name] = $namespaceBehaviors[$type]; + $prefix_map[str_replace('_', '/', $name) . '/'][$name] = $prefixBehaviors[$type]; + $this->registered[$name] = TRUE; + } + + $this->finder->getNamespaceMap()->registerDeepPaths($namespace_map); + $this->finder->getPrefixMap()->registerDeepPaths($prefix_map); + } +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/HookXautoload.php b/frontend/drupal/modules/xautoload/src/Phases/HookXautoload.php new file mode 100644 index 000000000..2f6215645 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/HookXautoload.php @@ -0,0 +1,111 @@ +system = $system; + } + + /** + * Wake up after a cache fail. + * + * @param ExtendedClassFinderInterface $finder + * @param string[] $extensions + * Extension type by extension name. + */ + public function wakeUp(ExtendedClassFinderInterface $finder, array $extensions) { + $this->finder = $finder; + $this->extensions = $extensions; + } + + /** + * Enter the boot phase of the request, where all bootstrap module files are included. + */ + public function enterBootPhase() { + // Nothing. + } + + /** + * Enter the main phase of the request, where all module files are included. + */ + public function enterMainPhase() { + $modules = $this->system->moduleImplements('xautoload'); + $this->runHookXautoload($modules); + } + + /** + * New extensions were enabled/installed. + * + * @param string $name + * Extension type by name. + * @param string $type + */ + public function welcomeNewExtension($name, $type) { + // Nothing. + } + + /** + * React to xautoload_modules_enabled() + * + * @param string[] $modules + * New module names. + */ + public function modulesEnabled($modules) { + $modules = $this->system->moduleImplements('xautoload'); + $this->runHookXautoload($modules); + } + + /** + * Runs hook_xautoload() on all enabled modules. + * + * This may occur multiple times in a request, if new modules are enabled. + * + * @param array $modules + */ + private function runHookXautoload(array $modules) { + // Let other modules register stuff to the finder via hook_xautoload(). + $adapter = \xautoload_InjectedAPI_hookXautoload::create($this->finder, ''); + foreach ($modules as $module) { + $adapter->setExtensionDir($dir = $this->system->drupalGetPath('module', $module)); + $function = $module . '_xautoload'; + $function($adapter, $dir); + } + } + +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/HookXautoloadEarly.php b/frontend/drupal/modules/xautoload/src/Phases/HookXautoloadEarly.php new file mode 100644 index 000000000..1306cfbf4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/HookXautoloadEarly.php @@ -0,0 +1,110 @@ +system = $system; + } + + /** + * Wake up after a cache fail. + * + * @param ExtendedClassFinderInterface $finder + * @param string[] $extensions + * Extension type by extension name. + */ + public function wakeUp(ExtendedClassFinderInterface $finder, array $extensions) { + $this->finder = $finder; + $this->extensions = $extensions; + } + + /** + * Enter the boot phase of the request, where all bootstrap module files are included. + */ + public function enterBootPhase() { + // @todo Call hook_xautoload() on bootstrap modules, if in bootstrap phase. + } + + /** + * Enter the main phase of the request, where all module files are included. + */ + public function enterMainPhase() { + // @todo Don't use moduleImplements(), to prevent hook_module_implements_alter() + $modules = $this->system->moduleImplements('xautoload'); + // @todo Remove boot modules from the list. + $this->runHookXautoload($modules); + } + + /** + * New extensions were enabled/installed. + * + * @param string $name + * Extension type by name. + * @param string $type + */ + public function welcomeNewExtension($name, $type) { + $function = $name . '_xautoload'; + if (!function_exists($function)) { + return; + } + $dir = $this->system->drupalGetPath($type, $name); + $adapter = \xautoload_InjectedAPI_hookXautoload::create($this->finder, $dir); + $function($adapter, $dir); + } + + /** + * React to xautoload_modules_enabled() + * + * @param string[] $modules + * New module names. + */ + public function modulesEnabled($modules) { + // Nothing. + } + + /** + * Runs hook_xautoload() on all enabled modules. + * + * This may occur multiple times in a request, if new modules are enabled. + * + * @param array $modules + */ + private function runHookXautoload(array $modules) { + // Let other modules register stuff to the finder via hook_xautoload(). + $adapter = \xautoload_InjectedAPI_hookXautoload::create($this->finder, ''); + foreach ($modules as $module) { + $adapter->setExtensionDir($dir = $this->system->drupalGetPath('module', $module)); + $function = $module . '_xautoload'; + $function($adapter, $dir); + } + } +} diff --git a/frontend/drupal/modules/xautoload/src/Phases/PhaseObserverInterface.php b/frontend/drupal/modules/xautoload/src/Phases/PhaseObserverInterface.php new file mode 100644 index 000000000..976fc5b59 --- /dev/null +++ b/frontend/drupal/modules/xautoload/src/Phases/PhaseObserverInterface.php @@ -0,0 +1,63 @@ +bar()" + */ + static function callbackToString($callback) { + if (is_array($callback)) { + list($obj, $method) = $callback; + if (is_object($obj)) { + $str = get_class($obj) . '->' . $method . '()'; + } + else { + $str = $obj . '::'; + $str .= $method . '()'; + } + } + else { + $str = $callback; + } + + return $str; + } + + /** + * Convert the underscores of a prefix into directory separators. + * + * @param string $prefix + * Prefix, without trailing underscore. + * + * @return string + * Path fragment representing the prefix, with trailing '/'. + */ + static function prefixLogicalPath($prefix) { + if (!strlen($prefix)) { + return ''; + } + $pear_logical_path = str_replace('_', '/', rtrim($prefix, '_') . '_'); + // Clean up surplus '/' resulting from duplicate underscores, or an + // underscore at the beginning of the class. + while (FALSE !== $pos = strrpos('/' . $pear_logical_path, '//')) { + $pear_logical_path[$pos] = '_'; + } + + return $pear_logical_path; + } + + /** + * Replace the namespace separator with directory separator. + * + * @param string $namespace + * Namespace without trailing namespace separator. + * + * @return string + * Path fragment representing the namespace, with trailing '/'. + */ + static function namespaceLogicalPath($namespace) { + return + strlen($namespace) + ? str_replace('\\', '/', rtrim($namespace, '\\') . '\\') + : ''; + } + + /** + * Check if a file exists, considering the full include path. + * Return the resolved path to that file. + * + * @param string $file + * The filepath + * @return boolean|string + * The resolved file path, if the file exists in the include path. + * FALSE, otherwise. + */ + static function findFileInIncludePath($file) { + if (function_exists('stream_resolve_include_path')) { + // Use the PHP 5.3.1+ way of doing this. + return stream_resolve_include_path($file); + } + elseif ($file[0] === '/') { + // That's an absolute path already. + return file_exists($file) + ? $file + : FALSE; + } + else { + // Manually loop all candidate paths. + foreach (explode(PATH_SEPARATOR, get_include_path()) as $base_dir) { + if (file_exists($base_dir . '/' . $file)) { + return $base_dir . '/' . $file; + } + } + + return FALSE; + } + } + + /** + * Checks whether an identifier is defined as either a class, interface or + * trait. Does not trigger autoloading. + * + * @param string $class + * @return bool + */ + static function classIsDefined($class) { + return class_exists($class, FALSE) + || interface_exists($class, FALSE) + || (PHP_VERSION_ID >= 50400 && trait_exists($class, FALSE)); + } + + /** + * Dummy method to force autoloading this class (or an ancestor). + */ + static function forceAutoload() { + // Do nothing. + } +} + diff --git a/frontend/drupal/modules/xautoload/tests/bootstrap.php b/frontend/drupal/modules/xautoload/tests/bootstrap.php new file mode 100644 index 000000000..7f4770260 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/bootstrap.php @@ -0,0 +1,37 @@ +finder->addPsr4('Drupal\xautoload\Tests\\', __DIR__ . '/src/'); + +// Use a non-cached class map generator. +xautoload()->getServiceContainer()->set('classMapGenerator', new ClassMapGenerator()); + +// Register a one-off class loader for the test PSR-4 classes. +/* +call_user_func( + function() { + $addPsr4 = function($namespace, $src) { + $strlen = strlen($namespace); + spl_autoload_register( + function ($class) use ($src, $namespace, $strlen) { + if (0 === strpos($class, $namespace)) { + $file = $src . '/' . str_replace('\\', '/', substr($class, $strlen)) . '.php'; + if (file_exists($file)) { + require_once $file; + return TRUE; + } + } + return FALSE; + } + ); + }; + $addPsr4('Drupal\xautoload\Tests\\', __DIR__ . '/src'); + $addPsr4('Drupal\xautoload\\', dirname(__DIR__) . '/src'); + } +); +*/ diff --git a/frontend/drupal/modules/xautoload/tests/fixtures/.libraries/ComposerTargetDirTestLib/Foo.php b/frontend/drupal/modules/xautoload/tests/fixtures/.libraries/ComposerTargetDirTestLib/Foo.php new file mode 100644 index 000000000..2b8fedacf --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/fixtures/.libraries/ComposerTargetDirTestLib/Foo.php @@ -0,0 +1,8 @@ +librariesLoad($name); +} diff --git a/frontend/drupal/modules/xautoload/tests/fixtures/.modules/system/system.module b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/system/system.module new file mode 100644 index 000000000..b3d9bbc7f --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/system/system.module @@ -0,0 +1 @@ +addPsr4('Drupal\testmod\\', 'psr4'); + new \Drupal\testmod\Foo(); +} + +/** + * Implements hook_libraries_info() + */ +function testmod_libraries_info() { + StaticCallLog::addCall(); + new \Drupal\testmod\Foo(); + return array( + 'testlib' => array( + 'name' => 'Test library', + 'xautoload' => '_testmod_libraries_testlib_xautoload', + ), + 'ComposerTestLib' => array( + 'xautoload' => '_testmod_libraries_ComposerTestLib_xautoload', + ), + 'ComposerTargetDirTestLib' => array( + 'xautoload' => '_testmod_libraries_ComposerTargetDirTestLib_xautoload', + ), + ); +} + +/** + * @param LocalDirectoryAdapter $adapter + */ +function _testmod_libraries_testlib_xautoload($adapter) { + StaticCallLog::addCall(); + $adapter->addPsr4('Acme\TestLib\\', 'src'); +} + +/** + * @param LocalDirectoryAdapter $adapter + */ +function _testmod_libraries_ComposerTestLib_xautoload($adapter) { + $adapter->composerJson('composer.json'); +} + +/** + * @param LocalDirectoryAdapter $adapter + */ +function _testmod_libraries_ComposerTargetDirTestLib_xautoload($adapter) { + $adapter->composerJson('composer.json'); +} diff --git a/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_pearflat/lib/Foo.php b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_pearflat/lib/Foo.php new file mode 100644 index 000000000..9890a23ef --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_pearflat/lib/Foo.php @@ -0,0 +1,3 @@ +main->registerModulePsr4(__FILE__, 'psr4'); + +function testmod_psr4_custom_init() { + StaticCallLog::addCall(); + new Foo; +} + +function testmod_psr4_custom_modules_enabled() { + StaticCallLog::addCall(); + new Foo; +} + +function testmod_psr4_custom_watchdog() { + StaticCallLog::addCall(); + new Foo; +} diff --git a/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_psr4_src/src/Foo.php b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_psr4_src/src/Foo.php new file mode 100644 index 000000000..35e1e6a81 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/fixtures/.modules/testmod_psr4_src/src/Foo.php @@ -0,0 +1,5 @@ +includeAllRecursivePsr4($lib, 'Drupal\xautoload', $skip); + } + + /** + * @param string $dir + * @param string $namespace + * @param array $skip + * + * @throws \Exception + */ + private function includeAllRecursivePsr4($dir, $namespace, array $skip) { + foreach (scandir($dir) as $candidate) { + if ('.' === $candidate || '..' === $candidate) { + continue; + } + $path = $dir . '/' . $candidate; + if (in_array($path, $skip)) { + continue; + } + if (is_dir($path)) { + $this->includeAllRecursivePsr4($dir . '/' . $candidate, $namespace . '\\' . $candidate, $skip); + } + elseif (is_file($path)) { + if ('.php' === substr($candidate, -4)) { + $class = $namespace . '\\' . substr($candidate, 0, -4); + if (class_exists($class)) { + continue; + } + if (interface_exists($class)) { + continue; + } + if (function_exists('trait_exists') && trait_exists($class)) { + continue; + } + throw new \Exception("Non-existing class, trait or interface '$class'."); + } + } + } + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/ClassFinderAdapterTest.php b/frontend/drupal/modules/xautoload/tests/src/ClassFinderAdapterTest.php new file mode 100644 index 000000000..cbb2c1ee3 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/ClassFinderAdapterTest.php @@ -0,0 +1,35 @@ +filesystem = StreamWrapper::register('test'); + } + + function tearDown() { + stream_wrapper_unregister('test'); + parent::tearDown(); + } + + /** + * Test hook_registry_files_alter() wildcard replacement. + */ + public function testWildcardClassmap() { + $this->filesystem->addClass('test://lib/xy/z.php', 'Foo\Bar'); + + $this->assertFalse(class_exists('Foo\Bar', FALSE), 'Class Foo\Bar must not exist yet.'); + xautoload()->adapter->addClassmapSources(array('test://lib/**/*.php')); + $this->assertTrue(class_exists('Foo\Bar'), 'Class Foo\Bar must exist.'); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/ClassLoaderTest.php b/frontend/drupal/modules/xautoload/tests/src/ClassLoaderTest.php new file mode 100644 index 000000000..b1b88c211 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/ClassLoaderTest.php @@ -0,0 +1,210 @@ +filesystem = StreamWrapper::register('test'); + } + + function tearDown() { + stream_wrapper_unregister('test'); + parent::tearDown(); + } + + // Test methods + // --------------------------------------------------------------------------- + + /** + * Test PSR-4-like namespaces. + */ + function testPsr4() { + + // Prepare the class finder. + $finder = new ClassFinder(); + + $finder->addPsr4('Drupal\ex_ample\\', 'test://base/lib/'); + + $this->assertCandidateOrder( + $finder, + 'Drupal\ex_ample\Psr4_%\Foo_Bar', + array('test://base/lib/Psr4_%/Foo_Bar.php')); + } + + /** + * Test PSR-0-like namespaces. + */ + function testNamespaces() { + + // Prepare the class finder. + $finder = new ClassFinder(); + $psr0 = new Psr0DirectoryBehavior(); + + $finder->registerNamespaceDeep('Drupal\\ex_ample', 'test://base/lib', $psr0); + $finder->registerNamespaceRoot('Drupal\\ex_ample', 'test://base/vendor', $psr0); + + $this->assertCandidateOrder( + $finder, + 'Drupal\ex_ample\Sub_%\Foo_Bar', + array( + 'test://base/lib/Sub_%/Foo/Bar.php', + 'test://base/vendor/Drupal/ex_ample/Sub_%/Foo/Bar.php', + )); + } + + /** + * Test PEAR-like prefixes. + */ + function testPrefixes() { + + // Prepare the class finder. + $finder = new ClassFinder(); + + $finder->registerPrefixDeep('ex_ample', 'test://base/lib'); + $finder->registerPrefixRoot('ex_ample', 'test://base/vendor'); + + $this->assertCandidateOrder( + $finder, + 'ex_ample_Sub%_Foo', + array( + 'test://base/lib/Sub%/Foo.php', + 'test://base/vendor/ex/ample/Sub%/Foo.php', + )); + } + + /** + * Tests PEAR-like class names beginning with underscore, or with a double + * underscore in between. + */ + function testSpecialUnderscores() { + + // Prepare the class finder. + $finder = new ClassFinder(); + + $finder->registerPrefixDeep('_ex_ample', 'test://lib'); + $finder->registerPrefixRoot('_ex_ample', 'test://vendor'); + + // Verify that underscores are not a problem.. + $this->assertCandidateOrder( + $finder, + '_ex_ample_Abc%_Def', array( + 'test://lib/Abc%/Def.php', + 'test://vendor/_ex/ample/Abc%/Def.php', + )); + $this->assertCandidateOrder($finder, '_abc_Foo%', array()); + $this->assertCandidateOrder($finder, 'abc__Foo%', array()); + } + + // Assertion helpers + // --------------------------------------------------------------------------- + + /** + * @param \Drupal\xautoload\ClassLoader\ClassLoaderInterface $loader + * @param string $class + * @param string $file + */ + protected function assertLoadClass($loader, $class, $file) { + + // Register the class file in the virtual filesystem. + $this->filesystem->addClass($file, $class); + + // Check that the class is not already defined. + $this->assertFalse(class_exists($class, FALSE)); + + // Trigger the class loader. + $loader->loadClass($class); + + // Check that the class is defined after the class loader has done its job. + $this->assertTrue(class_exists($class, FALSE)); + } + + /** + * @param \Drupal\xautoload\ClassLoader\ClassLoaderInterface $loader + * @param string $classTemplate + * @param string[] $expectedCandidateTemplates + */ + protected function assertCandidateOrder($loader, $classTemplate, array $expectedCandidateTemplates) { + for ($i = 0; $i < count($expectedCandidateTemplates); ++$i) { + $class = $this->replaceWildcard($classTemplate, "n$i"); + // If str_replace() is called with an array as 3rd parameter, it will do + // the replacement on all array elements. + $expectedCandidates = $this->replaceWildcardMultiple(array_slice($expectedCandidateTemplates, 0, $i + 1), "n$i"); + $this->assertFileInclusions($loader, $class, $expectedCandidates); + } + } + + /** + * Assert that inclusions are done in the expected order. + * + * @param \Drupal\xautoload\ClassLoader\ClassLoaderInterface $loader + * @param string $class + * @param string[] $expectedCandidates + */ + protected function assertFileInclusions($loader, $class, array $expectedCandidates) { + + // Register the class file in the virtual filesystem. + $this->filesystem->addClass(end($expectedCandidates), $class); + + $this->filesystem->resetReportedOperations(); + + // Check that the class is not already defined. + $this->assertFalse(class_exists($class, FALSE), "Class '$class' is not defined before loadClass()."); + + // Trigger the class loader. + $loader->loadClass($class); + + $expectedOperations = array(); + foreach ($expectedCandidates as $file) { + $expectedOperations[] = $file . ' - stat'; + } + $expectedOperations[] = end($expectedCandidates) . ' - include'; + $this->assertSame($expectedOperations, $this->filesystem->getReportedOperations()); + + // Check that the class is defined after the class loader has done its job. + $this->assertTrue(class_exists($class, FALSE), "Class is defined after loadClass()."); + } + + /** + * @param string[] $strings + * @param string $replacement + * + * @return string[] + */ + protected function replaceWildcardMultiple(array $strings, $replacement) { + foreach ($strings as &$str) { + $str = $this->replaceWildcard($str, $replacement); + } + return $strings; + } + + /** + * @param string $str + * @param string $replacement + * + * @return string + * + * @throws \Exception + */ + protected function replaceWildcard($str, $replacement) { + $fragments = explode('%', $str); + if (count($fragments) < 2) { + throw new \Exception("String '$str' does not contain a '%' wildcard."); + } + if (count($fragments) > 2) { + throw new \Exception("String '$str' has more than one '%' wildcard."); + } + return str_replace('%', $replacement, $str); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/ComposerJsonTest.php b/frontend/drupal/modules/xautoload/tests/src/ComposerJsonTest.php new file mode 100644 index 000000000..688a04964 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/ComposerJsonTest.php @@ -0,0 +1,35 @@ + array( + 'ComposerTestLib\Foo', + 'ComposerTestLib\Other\Foo', + ), + dirname(__DIR__) . '/fixtures/.libraries/ComposerTargetDirTestLib' => array( + 'Acme\ComposerTargetDirTestLib\Foo', + ), + ) as $dir => $classes) { + $localDirectoryAdapter = new LocalDirectoryAdapter($masterAdapter, $dir); + $localDirectoryAdapter->composerJson('composer.json'); + foreach ($classes as $class) { + $this->assertFalse(class_exists($class, FALSE), "Class $class not defined yet."); + $finder->loadClass($class); + $this->assertTrue(class_exists($class, FALSE), "Class $class is defined."); + } + } + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/DiscoveryTest.php b/frontend/drupal/modules/xautoload/tests/src/DiscoveryTest.php new file mode 100644 index 000000000..9ea0e08ef --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/DiscoveryTest.php @@ -0,0 +1,63 @@ + 'views', 'weight' => 0); + } + + // The class file is loaded using the regular uncached xautoload autoload. + $file_finder = new WildcardFileFinder(); + $file_finder->addDrupalPaths($files, TRUE); + $files = $file_finder->getDrupalFiles(); + + // The order of scandir() cannot be predicted, therefore only the sorted + // list of files is being compared here. + ksort($files); + + $expected = array ( + dirname(__DIR__) . '/fixtures/WildcardFileFinder/foo/bar.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/handlers/bar.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/handlers/foo.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/misc/abc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/misc/foo.bar', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/misc/sub/xyz', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/modules/sub/foo.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/modules/sub/sub/foo.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/modules/sub/sub/sub/foo.inc', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/tests/foo.test', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/tests/sub/foo.test', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/tests/sub/sub/foo.test', + dirname(__DIR__) . '/fixtures/WildcardFileFinder/tests/sub/sub/sub/foo.test', + ); + + $expected = array_fill_keys( + $expected, + array ( + 'module' => 'views', + 'weight' => 0, + )); + + $this->assertEquals($expected, $files); + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/AbstractDrupalBootTest.php b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/AbstractDrupalBootTest.php new file mode 100644 index 000000000..e3552e92c --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/AbstractDrupalBootTest.php @@ -0,0 +1,221 @@ +prepare(); + + $this->prepareAllEnabled(); + + $this->exampleDrupal->boot(); + + $expectedCalls = $this->getExpectedCallsForNormalRequest(); + + $this->callLog->assertCalls($this, $expectedCalls); + + // Now we want all classes to be available. + foreach ($this->exampleModules->getExampleClasses() as $classes) { + foreach ((array)$classes as $class) { + $this->assertClassExists($class); + } + } + + $this->unprepare(); + } + + /** + * Tests a request where modules are enabled, but xautoload is already + * enabled. + * + * @dataProvider providerModuleEnable + * + * @param mixed[] $initialModules + * Initial modules being installed / enabled. + * @param array $expectedCalls + * + * @throws \Exception + */ + function testModuleEnable(array $initialModules, array $expectedCalls) { + + $this->prepare(); + + $this->prepareInitialModules($initialModules); + + foreach ($this->exampleModules->getExampleClasses() as $classes) { + foreach ((array)$classes as $class) { + $this->assertClassIsUndefined($class); + } + } + + $this->exampleDrupal->boot(); + + $new_modules = array_keys($this->exampleModules->getExampleClasses()); + $this->exampleDrupal->moduleEnable($new_modules); + + # HackyLog::log($this->callLog->getCalls()); + + $this->callLog->assertCalls($this, $expectedCalls); + + // Now we want all classes to be available. + foreach ($this->exampleModules->getExampleClasses() as $classes) { + foreach ((array)$classes as $class) { + $this->assertClassExists($class); + } + } + + $this->unprepare(); + } + + /** + * @return array[] + */ + abstract public function providerModuleEnable(); + + /** + * Start with all available modules enabled. + */ + private function prepareAllEnabled() { + foreach (array('system', 'xautoload', 'libraries') as $name) { + $this->exampleDrupal->getSystemTable()->moduleSetEnabled($name); + } + foreach ($this->exampleModules->getExampleClasses() as $name => $classes) { + $this->exampleDrupal->getSystemTable()->moduleSetEnabled($name); + } + $this->exampleDrupal->getSystemTable()->moduleSetWeight('xautoload', -90); + } + + /** + * @param mixed[] $initialModules + * Initial modules being installed / enabled. + * + * @throws \Exception + */ + private function prepareInitialModules($initialModules) { + foreach ($initialModules as $name => $state) { + if (TRUE === $state) { + // Module is installed and enabled. + $this->exampleDrupal->getSystemTable()->moduleSetEnabled($name); + $this->exampleDrupal->getSystemTable()->moduleSetSchemaVersion($name, 7000); + } + elseif (FALSE === $state) { + // Module is installed, but disabled. + $this->exampleDrupal->getSystemTable()->moduleSetSchemaVersion($name, 7000); + } + elseif (NULL === $state) { + // Module is neither installed nor enabled. + } + else { + throw new \Exception("Unexpected state."); + } + } + if (isset($initialModules['xautoload'])) { + // xautoload is installed or enabled, so the module weight must be in the database. + $this->exampleDrupal->getSystemTable()->moduleSetWeight('xautoload', -90); + } + } + + /** + * setUp() does not help us because of the process sharing problem. + * So we use this instead. + * + * @throws \Exception + */ + abstract protected function prepare(); + + /** + * Runs after a test is finished. + */ + private function unprepare() { + stream_wrapper_unregister('test'); + StaticCallLog::unsetCallLog(); + } + + /** + * @param string $class + */ + public function assertLoadClass($class) { + $this->assertFalse(class_exists($class, FALSE), "Class '$class' is not defined yet."); + $this->assertTrue(class_exists($class), "Class '$class' successfully loaded."); + } + + /** + * @param string $class + */ + public function assertClassExists($class) { + $this->assertTrue(class_exists($class), "Class '$class' exists."); + } + + /** + * @param string $class + */ + public function assertClassIsDefined($class) { + $this->assertTrue(class_exists($class, FALSE), "Class '$class' is defined."); + } + + /** + * @param string $class + */ + public function assertClassIsUndefined($class) { + $this->assertFalse(class_exists($class, FALSE), "Class '$class' is undefined."); + } + + /** + * @return array[] + */ + abstract protected function getExpectedCallsForNormalRequest(); + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootHookTest.php b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootHookTest.php new file mode 100644 index 000000000..5b57b696b --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootHookTest.php @@ -0,0 +1,172 @@ + TRUE)); + foreach (array( + 'xautoload' => array(FALSE, TRUE), + 'libraries' => array(FALSE, TRUE), + 'testmod' => array(FALSE, NULL), + ) as $module => $states) { + $initialModuleVariations = $this->providerArrayKeyVariations($initialModuleVariations, $module, $states); + } + $variations = array(); + foreach ($initialModuleVariations as $initialModuleVariation) { + $expectedCalls = array(); + + if ($hookXautoloadEarly) { + $expectedCalls[] = array( + 'function' => 'testmod_xautoload', + 'args' => array( + '(xautoload_InjectedAPI_hookXautoload)', + dirname(dirname(__DIR__)) . '/fixtures/.modules/testmod', + ), + ); + } + + if (NULL === $initialModuleVariation['testmod']) { + $expectedCalls[] = array( + 'function' => 'testmod_schema', + 'args' => array(), + ); + $expectedCalls[] = array( + 'function' => 'testmod_install', + 'args' => array(), + ); + $expectedCalls[] = array( + 'function' => 'testmod_watchdog', + 'args' => array(), + ); + } + + $expectedCalls[] = array( + 'function' => 'testmod_enable', + 'args' => array(), + ); + $expectedCalls[] = array( + 'function' => 'testmod_watchdog', + 'args' => array(), + ); + + if ($hookXautoloadLate) { + $expectedCalls[] = array( + 'function' => 'testmod_xautoload', + 'args' => array( + '(xautoload_InjectedAPI_hookXautoload)', + dirname(dirname(__DIR__)) . '/fixtures/.modules/testmod', + ), + ); + } + $expectedCalls[] = array( + 'function' => 'testmod_modules_enabled', + 'args' => array( + '(array)' + ), + ); + $expectedCalls[] = array( + 'function' => 'testmod_libraries_info', + 'args' => array(), + ); + $expectedCalls[] = array( + 'function' => '_testmod_libraries_testlib_xautoload', + 'args' => array( + '(xautoload_InjectedAPI_hookXautoload)', + dirname(dirname(__DIR__)) . '/fixtures/.libraries/testlib', + ), + ); + + $variations[] = array($initialModuleVariation, $expectedCalls); + } + return $variations; + } + + function initOnce() { + if (isset($this->exampleDrupal)) { + return; + } + $this->exampleModules = new HookTestExampleModules(); + $this->exampleDrupal = new DrupalEnvironment($this->exampleModules); + $this->exampleDrupal->setStaticInstance(); + } + + /** + * setUp() does not help us because of the process sharing problem. + * So we use this instead. + * + * @throws \Exception + */ + protected function prepare() { + $this->initOnce(); + $filesystem = StreamWrapper::register('test'); + foreach ($this->exampleModules->discoverModuleFilenames('module') as $name => $filename) { + $this->exampleDrupal->getSystemTable()->addModuleWithFilename($name, $filename); + } + $this->exampleDrupal->getSystemTable()->moduleSetEnabled('system'); + $this->exampleDrupal->initBootstrapStatus(); + # $this->exampleDrupal->getCache()->cacheSet('module_implements', $data, 'cache_bootstrap'); + xautoload()->getServiceContainer()->set('system', $this->exampleDrupal->getMockDrupalSystem()); + $this->callLog = new CallLog(); + StaticCallLog::setCallLog($this->callLog); + } + + /** + * @return array[] + */ + protected function getExpectedCallsForNormalRequest() { + $expectedCalls = array( + array( + 'function' => 'testmod_xautoload', + 'args' => array( + '(xautoload_InjectedAPI_hookXautoload)', + dirname(dirname(__DIR__)) . '/fixtures/.modules/testmod', + # 'test://modules/testmod', + ), + ), + array( + 'function' => 'testmod_init', + 'args' => array(), + ), + array( + 'function' => 'testmod_libraries_info', + 'args' => array(), + ), + array( + 'function' => '_testmod_libraries_testlib_xautoload', + 'args' => array( + '(xautoload_InjectedAPI_hookXautoload)', + dirname(dirname(__DIR__)) . '/fixtures/.libraries/testlib', + # 'test://libraries/testlib', + ), + ), + ); + return $expectedCalls; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootTest.php b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootTest.php new file mode 100644 index 000000000..80b5fe6ed --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/DrupalBootTest/DrupalBootTest.php @@ -0,0 +1,157 @@ + TRUE, + * 'xautoload' => FALSE, + * 'libraries' => NULL), ...) + */ + private function initialModulesVariations($install) { + $variations = array(); + $state = $install ? NULL : FALSE; + $variation = array('system' => TRUE); + $variation += array_fill_keys(array_keys($this->exampleModules->getExampleClasses()), $state); + $variations[] = $variation; + foreach (array('xautoload') as $module) { + $variations = $this->providerArrayKeyVariations($variations, $module, array(TRUE, FALSE, NULL)); + } + return $variations; + } + + /** + * @return array[] + */ + public function providerModuleEnable() { + $this->initOnce(); + $variations = array(); + foreach (array(TRUE, FALSE) as $install) { + $expectedCalls = array(); + $enabledModulesSoFar = array(); + foreach ($this->exampleModules->getExampleClasses() as $module => $classes) { + $enabledModulesSoFar[] = $module; + if ($install) { + $expectedCalls[] = array( + 'function' => $module . '_schema', + 'args' => array(), + ); + $expectedCalls[] = array( + 'function' => $module . '_install', + 'args' => array(), + ); + foreach ($enabledModulesSoFar as $module) { + $expectedCalls[] = array( + 'function' => $module . '_watchdog', + 'args' => array(), + ); + } + } + $expectedCalls[] = array( + 'function' => $module . '_enable', + 'args' => array(), + ); + foreach ($enabledModulesSoFar as $module) { + $expectedCalls[] = array( + 'function' => $module . '_watchdog', + 'args' => array(), + ); + } + } + foreach ($this->initialModulesVariations($install) as $moduleStates) { + /* + $enabledModules = array(); + foreach ($moduleStates as $module => $state) { + if (TRUE !== $state) { + $enabledModules[$module] = TRUE; + } + } + foreach ($enabledModulesSoFar as $module) { + if (isset($enabledModules[$module])) { + unset($enabledModules[$module]); + $enabledModules[$module] = TRUE; + } + } + $enabledModules = array_keys($enabledModules); + */ + $variationExpectedCalls = $expectedCalls; + foreach (array_keys($this->exampleModules->getExampleClasses()) as $module) { + $variationExpectedCalls[] = array( + 'function' => $module . '_modules_enabled', + 'args' => array('(array)'), + ); + } + $variations[] = array($moduleStates, $variationExpectedCalls); + } + } + + return $variations; + } + + function initOnce() { + if (isset($this->exampleDrupal)) { + return; + } + $this->exampleModules = new ExampleModules(); + $this->exampleDrupal = new DrupalEnvironment($this->exampleModules); + $this->exampleDrupal->setStaticInstance(); + } + + /** + * setUp() does not help us because of the process sharing problem. + * So we use this instead. + * + * @throws \Exception + */ + protected function prepare() { + $this->initOnce(); + $filesystem = StreamWrapper::register('test'); + foreach ($this->exampleModules->discoverModuleFilenames('module') as $name => $filename) { + $this->exampleDrupal->getSystemTable()->addModuleWithFilename($name, $filename); + } + $this->exampleDrupal->getSystemTable()->moduleSetEnabled('system'); + $this->exampleDrupal->initBootstrapStatus(); + # $this->exampleDrupal->getCache()->cacheSet('module_implements', $data, 'cache_bootstrap'); + xautoload()->getServiceContainer()->set('system', $this->exampleDrupal->getMockDrupalSystem()); + $this->callLog = new CallLog(); + StaticCallLog::setCallLog($this->callLog); + } + + /** + * @return array[] + */ + protected function getExpectedCallsForNormalRequest() { + $expectedCalls = array(); + foreach ($this->exampleModules->getExampleClasses() as $module => $classes) { + $expectedCalls[] = array( + 'function' => $module . '_init', + 'args' => array(), + ); + } + return $expectedCalls; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Example/AbstractExampleModules.php b/frontend/drupal/modules/xautoload/tests/src/Example/AbstractExampleModules.php new file mode 100644 index 000000000..172a740cd --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Example/AbstractExampleModules.php @@ -0,0 +1,106 @@ +getAvailableExtensions() as $name => $type) { + if ('module' !== $type) { + continue; + } + $modules[$name] = (object)array( + 'uri' => $this->getExtensionFilename($type, $name), + 'filename' => $name . '.module', + 'name' => $name, + ); + } + return $modules; + } + + /** + * @return string[] + * Extension types by name. + */ + abstract protected function getAvailableExtensions(); + + /** + * @return true[] + */ + public function getBootstrapModules() { + $bootstrap_modules = array(); + foreach ($this->discoverModuleFilenames('module') as $name => $filename) { + $php = file_get_contents($filename); + foreach (PureFunctions::bootstrapHooks() as $hook) { + if (FALSE !== strpos($php, 'function ' . $name . '_' . $hook)) { + $bootstrap_modules[$name] = TRUE; + break; + } + } + } + return $bootstrap_modules; + } + + /** + * @param \Drupal\xautoload\Tests\DrupalBootTest\AbstractDrupalBootTest $testCase + */ + public function assertLoadExampleClasses(AbstractDrupalBootTest $testCase) { + foreach ($this->getExampleClasses() as $class) { + $testCase->assertLoadClass($class); + } + } + + /** + * @return array[] + */ + abstract public function getExampleClasses(); + + /** + * @param string $type + * E.g. 'module' + * + * @return string[] + */ + function discoverModuleFilenames($type) { + $filenames = array(); + foreach ($this->getAvailableExtensions() as $name => $itemType) { + if ($type !== $itemType) { + continue; + } + $filenames[$name] = $this->getExtensionFilename($type, $name); + } + return $filenames; + } + + /** + * @param string $type + * @param string $name + * + * @return string + */ + public function getExtensionFilename($type, $name) { + if ('xautoload' === $name) { + return dirname(dirname(dirname(__DIR__))) . '/xautoload.module'; + } + $file = dirname(dirname(__DIR__)) . '/fixtures/.modules/' . $name . '/' . $name . '.module'; + if (is_file($file)) { + return $file; + } + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Example/ExampleModules.php b/frontend/drupal/modules/xautoload/tests/src/Example/ExampleModules.php new file mode 100644 index 000000000..647a19bc4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Example/ExampleModules.php @@ -0,0 +1,52 @@ + 'testmod_pearflat_Foo', + 'testmod_psr0_lib' => 'Drupal\testmod_psr0_lib\Foo', + 'testmod_psr4_custom' => 'Drupal\testmod_psr4_custom\Foo', + 'testmod_psr4_src' => 'Drupal\testmod_psr4_src\Foo', + ); + } + + /** + * Replicates drupal_parse_info_file(dirname($module->uri) . '/' . $module->name . '.info') + * + * @see drupal_parse_info_file() + * + * @param string $name + * + * @return array + * Parsed info file contents. + */ + public function drupalParseInfoFile($name) { + $info = array('core' => '7.x'); + if (0 === strpos($name, 'testmod')) { + $info['dependencies'][] = 'xautoload'; + } + return $info; + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Example/HookTestExampleModules.php b/frontend/drupal/modules/xautoload/tests/src/Example/HookTestExampleModules.php new file mode 100644 index 000000000..59871d244 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Example/HookTestExampleModules.php @@ -0,0 +1,56 @@ + array( + 'Drupal\\testmod\\Foo', + 'Acme\\TestLib\\Foo', + ), + ); + } + + /** + * Replicates drupal_parse_info_file(dirname($module->uri) . '/' . $module->name . '.info') + * + * @see drupal_parse_info_file() + * + * @param string $name + * + * @return array + * Parsed info file contents. + */ + public function drupalParseInfoFile($name) { + $info = array('core' => '7.x'); + if ('testmod' === $name) { + $info['dependencies'][] = 'xautoload'; + $info['dependencies'][] = 'libraries'; + } + return $info; + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Filesystem/StreamWrapper.php b/frontend/drupal/modules/xautoload/tests/src/Filesystem/StreamWrapper.php new file mode 100644 index 000000000..6eb23e802 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Filesystem/StreamWrapper.php @@ -0,0 +1,159 @@ +getStat($path); + } + + /** + * @param $path + * @param $mode + * @param $options + * @param $opened_path + * + * @return bool + * + * @throws \Exception + */ + function stream_open($path, $mode, $options, &$opened_path) { + + $this->contents = self::$filesystem->getFileContents($path); + + $this->path = $path; + $this->position = 0; + + return TRUE; + } + + /** + * @return array + * Stat for the currently open stream. + * @throws \Exception + */ + function stream_stat() { + if (!isset($this->path)) { + throw new \Exception("No file currently open."); + } + return self::$filesystem->getStat($this->path, FALSE); + } + + /** + * @param int $count + * Number of characters to read. + * + * @return string + * Snippet read from the file. + */ + function stream_read($count) { + $ret = substr($this->contents, $this->position, $count); + $this->position += strlen($ret); + + return $ret; + } + + /** + * @return bool + */ + function stream_eof() { + return $this->position >= strlen($this->contents); + } + + /** + * @param string $path + * @param int $options + * + * @return bool + */ + function dir_opendir($path, $options) { + $contents = self::$filesystem->getDirContents($path); + if (FALSE === $contents) { + return FALSE; + } + $this->path = $path; + $this->dirContents = $contents; + return TRUE; + } + + /** + * @return string + */ + function dir_readdir() { + $name = current($this->dirContents); + next($this->dirContents); + return $name; + } +} \ No newline at end of file diff --git a/frontend/drupal/modules/xautoload/tests/src/Filesystem/VirtualFilesystem.php b/frontend/drupal/modules/xautoload/tests/src/Filesystem/VirtualFilesystem.php new file mode 100644 index 000000000..ca8787dc1 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Filesystem/VirtualFilesystem.php @@ -0,0 +1,273 @@ +reportedOperations[] = $file . ' - include'; + } + + /** + * @var string + */ + protected $instanceKey; + + /** + * @var string[] + */ + protected $knownPaths = array(); + + /** + * @var string[] + */ + protected $reportedOperations = array(); + + const NOTHING = FALSE; + const DIR = '(dir)'; + const FILE = '(file)'; + + function __construct() { + $this->instanceKey = Util::randomString(); + self::$instances[$this->instanceKey] = $this; + } + + /** + * @return array[] + */ + function getReportedOperations() { + return $this->reportedOperations; + } + + /** + * Delete all reported operations and start fresh. + */ + function resetReportedOperations() { + $this->reportedOperations = array(); + } + + /** + * @param string $file + * @param string $class + * @throws \Exception + */ + function addClass($file, $class) { + $this->addKnownFile($file); + if (self::FILE !== ($existing = $this->knownPaths[$file])) { + throw new \Exception("A non-empty file already exists at '$file'. Cannot overwrite with class '$class'."); + } + $this->knownPaths[$file] = $class; + } + + /** + * @param string $file + * @param string $php + * File contents starting with 'addKnownFile($file); + if (!$overwrite && self::FILE !== ($existing = $this->knownPaths[$file])) { + throw new \Exception("A non-empty file already exists at '$file'. Cannot overwrite with PHP code."); + } + if (0 !== strpos($php, 'knownPaths[$file] = $php; + } + + /** + * @param string[] $files + */ + function addKnownFiles(array $files) { + foreach ($files as $file) { + $this->addKnownFile($file); + } + } + + /** + * @param string $file + * + * @throws \Exception + */ + function addKnownFile($file) { + if (!isset($this->knownPaths[$file])) { + $this->knownPaths[$file] = self::FILE; + $this->addKnownDir(dirname($file)); + } + elseif (self::DIR === $this->knownPaths[$file]) { + throw new \Exception("A directory already exists at '$file', cannot overwrite with a file."); + } + } + + /** + * @param string $dir + */ + function addKnownDir($dir) { + if (FALSE === strpos($dir, '://')) { + return; + } + if (!isset($this->knownPaths[$dir])) { + // Need to set parents first. + $this->addKnownDir(dirname($dir)); + } + $this->knownPaths[$dir] = self::DIR; + } + + /** + * @param string $path + * @return string|bool + * One of self::NOTHING, self::DIR, self::FILE, or a class name for a class + * that is supposed to be defined in the file. + */ + function resolvePath($path) { + if (isset($this->knownPaths[$path])) { + return $this->knownPaths[$path]; + } + else { + return self::NOTHING; + } + } + + /** + * @param string $dir + * @return array|bool + */ + function getDirContents($dir) { + if (empty($this->knownPaths[$dir]) || self::DIR !== $this->knownPaths[$dir]) { + return FALSE; + } + $pos = strlen($dir . '/'); + $contents = array('.', '..'); + foreach ($this->knownPaths as $path => $type) { + if ($dir . '/' !== substr($path, 0, $pos)) { + continue; + } + $name = substr($path, $pos); + if (FALSE !== strpos($name, '/')) { + // This is a deeper subdirectory. + continue; + } + if ('' === $name) { + continue; + } + $contents[] = $name; + } + return $contents; + } + + /** + * @param string $path + * @param bool $report + * + * @return array + */ + function getStat($path, $report = TRUE) { + if ($report) { + $this->reportedOperations[] = $path . ' - stat'; + } + if (!isset($this->knownPaths[$path])) { + // File does not exist. + return FALSE; + } + elseif (self::DIR === $this->knownPaths[$path]) { + return stat(__DIR__); + } + else { + // Create a tmp file with the contents and get its stats. + $contents = $this->getFileContents($path); + $resource = tmpfile(); + fwrite($resource, $contents); + $stat = fstat($resource); + fclose($resource); + return $stat; + } + } + + /** + * @param $path + * The file path. + * + * @return string + * The file contents. + * + * @throws \Exception + * Exception thrown if there is no file at $path. + */ + function getFileContents($path) { + if (!isset($this->knownPaths[$path])) { + // File does not exist. + throw new \Exception("Assumed file '$path' does not exist."); + } + elseif (self::DIR === $this->knownPaths[$path]) { + throw new \Exception("Assumed file '$path' is a directory."); + } + + $instance_key_export = var_export($this->instanceKey, TRUE); + $path_export = var_export($path, TRUE); + if (self::FILE === $this->knownPaths[$path]) { + // Empty PHP file.. + return <<knownPaths[$path], 'knownPaths[$path], 5); + return <<knownPaths[$path])) { + // File with arbitrary contents. + return $this->knownPaths[$path]; + } + + // PHP file with class definition. + $class = $this->knownPaths[$path]; + + if (FALSE === ($pos = strrpos($class, '\\'))) { + // Class without namespace. + return <<components = $components; + } + + /** + * {@inheritdoc} + */ + function variableSet($name, $value) { + $this->variables[$name] = $value; + } + + /** + * {@inheritdoc} + */ + function variableGet($name, $default = NULL) { + return isset($this->variables[$name]) + ? $this->variables[$name] + : $default; + } + + /** + * {@inheritdoc} + */ + function drupalGetFilename($type, $name) { + return $this->components->DrupalGetFilename->drupalGetFilename($type, $name); + } + + /** + * {@inheritdoc} + */ + function drupalGetPath($type, $name) { + return $this->components->DrupalGetFilename->drupalGetPath($type, $name); + } + + /** + * {@inheritdoc} + */ + function getExtensionTypes($extension_names) { + // Simply assume that everything is a module. + return array_fill_keys($extension_names, 'module'); + } + + /** + * {@inheritdoc} + */ + function getActiveExtensions() { + return $this->components->SystemTable->getActiveExtensions(); + } + + /** + * Replicates module_list() + * + * @param bool $refresh + * @param bool $bootstrap_refresh + * @param bool $sort + * + * @return string[] + */ + function moduleList($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE) { + return $this->components->ModuleList->moduleList($refresh, $bootstrap_refresh, $sort); + } + + /** + * @see module_invoke() + * + * @param string $module + * @param string $hook + * + * @return mixed + * + * @throws \Exception + */ + function moduleInvoke($module, $hook) { + $args = func_get_args(); + switch (count($args)) { + case 2: + return PureFunctions::moduleInvoke($module, $hook); + case 3: + return PureFunctions::moduleInvoke($module, $hook, $args[2]); + case 4: + return PureFunctions::moduleInvoke($module, $hook, $args[2], $args[3]); + default: + throw new \Exception("More arguments than expected."); + } + } + + /** + * @param string $hook + */ + function moduleInvokeAll($hook) { + $args = func_get_args(); + call_user_func_array(array($this->components->HookSystem, 'moduleInvokeAll'), $args); + } + + /** + * @param string $hook + * + * @throws \Exception + * @return array + */ + function moduleImplements($hook) { + return $this->components->HookSystem->moduleImplements($hook); + } + + /** + * @param string $hook + * @param mixed $data + */ + function drupalAlter($hook, &$data) { + $args = func_get_args(); + assert($hook === array_shift($args)); + assert($data === array_shift($args)); + while (count($args) < 3) { + $args[] = NULL; + } + $this->components->HookSystem->drupalAlter($hook, $data, $args[0], $args[1], $args[2]); + } + + /** + * Replicates module_load_include() + * + * @param string $type + * @param string $module + * @param string|null $name + * + * @return bool|string + */ + function moduleLoadInclude($type, $module, $name = NULL) { + if (!isset($name)) { + $name = $module; + } + $file = $this->drupalGetPath('module', $module) . "/$name.$type"; + if (is_file($file)) { + require_once $file; + return $file; + } + return FALSE; + } + + /** + * Resets the module_implements() cache. + */ + public function resetModuleImplementsCache() { + $this->components->HookSystem->moduleImplementsReset(); + } + + /** + * @see libraries_info() + * + * @return mixed + */ + function getLibrariesInfo() { + $this->components->LibrariesInfo->resetLibrariesInfo(); + return $this->components->LibrariesInfo->getLibrariesInfo(); + } + + /** + * @see libraries_get_path() + * + * @param string $name + * Name of the library. + * + * @return string|false + */ + function librariesGetPath($name) { + return $this->components->LibrariesInfo->librariesGetPath($name); + } + + /** + * Called from xautoload_install() to set the module weight. + * + * @param int $weight + * New module weight for xautoload. + */ + public function installSetModuleWeight($weight) { + $this->components->SystemTable->moduleSetWeight('xautoload', $weight); + $this->components->SystemListReset->systemListReset(); + } + + /** + * @param string $cid + * @param string $bin + * + * @return object|false + * The cache or FALSE on failure. + * + * @see cache_get() + */ + public function cacheGet($cid, $bin = 'cache') { + return $this->components->Cache->cacheGet($cid, $bin); + } + + /** + * @param string $cid + * @param mixed $data + * @param string $bin + * + * @return mixed + * + * @see cache_set() + */ + public function cacheSet($cid, $data, $bin = 'cache') { + $this->components->Cache->cacheSet($cid, $data, $bin); + } + + /** + * @param string|null $cid + * @param string|null $bin + * + * @see cache_clear_all() + */ + public function cacheClearAll($cid = NULL, $bin = NULL) { + $this->components->Cache->cacheClearAll($cid, $bin); + } + + /** + * @param string $key + */ + public function drupalStaticReset($key) { + $this->components->DrupalStatic->resetKey($key); + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Util/CallLog.php b/frontend/drupal/modules/xautoload/tests/src/Util/CallLog.php new file mode 100644 index 000000000..72911a7e0 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Util/CallLog.php @@ -0,0 +1,75 @@ +calls[] = $call; + } + + /** + * @return array[] + */ + function getCalls() { + return $this->calls; + } + + /** + * @param \PHPUnit_Framework_TestCase $testCase + * @param array[] $expectedCalls + */ + function assertCalls(\PHPUnit_Framework_TestCase $testCase, array $expectedCalls) { + if (array_values($expectedCalls) !== $expectedCalls) { + throw new \InvalidArgumentException('$expectedCalls must be a numeric array with no keys missing.'); + } + $extractFunction = array($this, 'callGetFunction'); + $testCase->assertEquals( + "\n" . implode("\n", array_map($extractFunction, $expectedCalls)) . "\n", + "\n" . implode("\n", array_map($extractFunction, $this->calls)) . "\n"); + $testCase->assertEquals($expectedCalls, $this->calls); + for ($i = 0; TRUE; ++$i) { + $actualCall = isset($this->calls[$i]) ? $this->calls[$i] : NULL; + $expectedCall = isset($expectedCalls[$i]) ? $expectedCalls[$i] : NULL; + if (NULL === $actualCall && NULL === $expectedCall) { + break; + } + if (NULL === $actualCall) { + $testCase->fail("Call $i missing.\nExpected: " . var_export($expectedCall, TRUE)); + break; + } + if (NULL === $expectedCall) { + $testCase->fail("Call $i was not expected.\nActual: " . var_export($actualCall, TRUE)); + break; + } + if ($actualCall !== $expectedCall) { + $testCase->fail("Call $i mismatch.\nExpected: " . var_export($expectedCall, TRUE) . "\nActual: " . var_export($this->calls[$i], TRUE)); + break; + } + } + $testCase->assertEquals($expectedCalls, $this->calls); + } + + function callGetFunction($call) { + if (!isset($call['function'])) { + return NULL; + } + if (!isset($call['class'])) { + return $call['function']; + } + return $call['class'] . '::' . $call['function']; + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/Util/HackyLog.php b/frontend/drupal/modules/xautoload/tests/src/Util/HackyLog.php new file mode 100644 index 000000000..63452fbfe --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/Util/HackyLog.php @@ -0,0 +1,23 @@ +=') + ? debug_backtrace(0, 2) + // Second parameter not supported in PHP < 5.4.0. It would cause a + // "Warning: debug_backtrace() expects at most 1 parameter, 2 given". + : debug_backtrace(0); + + $call = $trace[1]; + $callFiltered = array(); + foreach (array('function', 'class', 'type') as $key) { + if (isset($call[$key])) { + $callFiltered[$key] = $call[$key]; + } + } + $callFiltered['args'] = array(); + foreach ($call['args'] as $arg) { + if (is_array($arg)) { + $arg = '(array)'; + } + elseif (is_object($arg)) { + $arg = '(' . get_class($arg) . ')'; + } + $callFiltered['args'][] = $arg; + } + self::$callLog->addCall($callFiltered); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/Cache.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/Cache.php new file mode 100644 index 000000000..5254d7eb7 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/Cache.php @@ -0,0 +1,68 @@ +cache[$bin][$cid])) { + return FALSE; + } + return $this->cache[$bin][$cid]; + } + + /** + * @param string $cid + * @param mixed $data + * @param string $bin + * + * @see cache_set() + */ + function cacheSet($cid, $data, $bin = 'cache') { + $this->cache[$bin][$cid] = (object)array( + 'data' => $data, + ); + } + + /** + * @param null $cid + * @param null $bin + * + * @return mixed + * + * @see cache_clear_all() + */ + function cacheClearAll($cid = NULL, $bin = NULL) { + if (!isset($cid) && !isset($bin)) { + $this->cacheClearAll(NULL, 'cache_page'); + return NULL; + } + elseif (!isset($cid)) { + unset($this->cache[$bin]); + } + elseif (!isset($bin)) { + throw new \InvalidArgumentException("No cache \$bin argument given."); + } + else { + unset($this->cache[$bin][$cid]); + } + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalBootstrap.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalBootstrap.php new file mode 100644 index 000000000..38932f86f --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalBootstrap.php @@ -0,0 +1,120 @@ +drupalLoad = $drupalLoad; + $this->hookSystem = $hookSystem; + $this->moduleList = $moduleList; + } + + /** + * @see drupal_bootstrap() + */ + function boot() { + $this->drupalBootstrapVariables(); + $this->drupalBootstrapPageHeader(); + $this->drupalBootstrapFull(); + } + + /** + * @see _drupal_bootstrap_variables() + */ + private function drupalBootstrapVariables() { + $this->moduleLoadAll(TRUE); + } + + /** + * @see _drupal_bootstrap_page_header() + */ + private function drupalBootstrapPageHeader() { + $this->bootstrapInvokeAll('boot'); + } + + /** + * @see _drupal_bootstrap_full() + */ + private function drupalBootstrapFull() { + $this->moduleLoadAll(); + $this->menuSetCustomTheme(); + $this->hookSystem->moduleInvokeAll('init'); + } + + /** + * @see menu_set_custom_theme() + */ + private function menuSetCustomTheme() { + $this->hookSystem->moduleInvokeAll('custom_theme'); + } + + /** + * Replicates module_load_all() + * + * @see module_load_all() + * + * @param bool|null $bootstrap + * + * @return bool + */ + private function moduleLoadAll($bootstrap = FALSE) { + if (isset($bootstrap)) { + foreach ($this->moduleList->moduleList(TRUE, $bootstrap) as $module) { + $this->drupalLoad->drupalLoad('module', $module); + } + // $has_run will be TRUE if $bootstrap is FALSE. + $this->moduleLoadAllHasRun = !$bootstrap; + } + return $this->moduleLoadAllHasRun; + } + + /** + * @see bootstrap_invoke_all() + * + * @param string $hook + */ + private function bootstrapInvokeAll($hook) { + // Bootstrap modules should have been loaded when this function is called, so + // we don't need to tell module_list() to reset its internal list (and we + // therefore leave the first parameter at its default value of FALSE). We + // still pass in TRUE for the second parameter, though; in case this is the + // first time during the bootstrap that module_list() is called, we want to + // make sure that its internal cache is primed with the bootstrap modules + // only. + foreach ($this->moduleList->moduleList(FALSE, TRUE) as $module) { + $this->drupalLoad->drupalLoad('module', $module); + PureFunctions::moduleInvoke($module, $hook); + } + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalComponentContainer.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalComponentContainer.php new file mode 100644 index 000000000..e0d7f6d7a --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalComponentContainer.php @@ -0,0 +1,268 @@ +exampleModules = $exampleModules; + } + + /** + * Magic getter for a Drupal component. + * + * @param string $key + * + * @return object + * + * @throws \Exception + */ + function __get($key) { + if (array_key_exists($key, $this->components)) { + return $this->components[$key]; + } + $method = 'get' . $key; + if (!method_exists($this, $method)) { + throw new \Exception("Unsupported key '$key' for DrupalComponentContainer."); + } + return $this->components[$key] = $this->$method($this); + } + + /** + * @return SystemTable + * + * @see DrupalComponentContainer::SystemTable + */ + protected function getSystemTable() { + return new SystemTable(); + } + + /** + * @return Cache + * + * @see DrupalComponentContainer::Cache + */ + protected function getCache() { + return new Cache(); + } + + /** + * @return DrupalStatic + * + * @see DrupalComponentContainer::DrupalStatic + */ + protected function getDrupalStatic() { + return new DrupalStatic(); + } + + /** + * @return DrupalGetFilename + * + * @see DrupalComponentContainer::DrupalGetFilename + */ + protected function getDrupalGetFilename() { + return new DrupalGetFilename($this->SystemTable, $this->exampleModules); + } + + /** + * @return HookSystem + * + * @see DrupalComponentContainer::HookSystem + */ + protected function getHookSystem() { + return new HookSystem( + $this->DrupalStatic, + $this->Cache, + $this->ModuleList); + } + + /** + * @return ModuleEnable + * + * @see DrupalComponentContainer::ModuleEnable + */ + protected function getModuleEnable() { + return new ModuleEnable( + $this->DrupalGetFilename, + $this->HookSystem, + $this->ModuleList, + $this->SystemTable, + $this->SystemListReset, + $this->SystemRebuildModuleData, + $this->SystemUpdateBootstrapStatus); + } + + /** + * @return ModuleList + * + * @see DrupalComponentContainer::ModuleList + */ + protected function getModuleList() { + return new ModuleList( + $this->DrupalGetFilename, + $this->SystemList, + $this->DrupalStatic); + } + + /** + * @return SystemListReset + * + * @see DrupalComponentContainer::SystemListReset + */ + protected function getSystemListReset() { + return new SystemListReset( + $this->Cache, + $this->DrupalStatic); + } + + /** + * @return ModuleBuildDependencies + * + * @see DrupalComponentContainer::ModuleBuildDependencies + */ + protected function getModuleBuildDependencies() { + return new ModuleBuildDependencies(); + } + + /** + * @return SystemBuildModuleData + * + * @see DrupalComponentContainer::SystemBuildModuleData + */ + protected function getSystemBuildModuleData() { + return new SystemBuildModuleData( + $this->exampleModules, + $this->HookSystem); + } + + /** + * @return SystemRebuildModuleData + * + * @see DrupalComponentContainer::SystemRebuildModuleData + */ + protected function getSystemRebuildModuleData() { + return new SystemRebuildModuleData( + $this->DrupalStatic, + $this->ModuleBuildDependencies, + $this->SystemTable, + $this->SystemBuildModuleData, + $this->SystemListReset); + } + + /** + * @return SystemUpdateBootstrapStatus + * + * @see DrupalComponentContainer::SystemUpdateBootstrapStatus + */ + protected function getSystemUpdateBootstrapStatus() { + return new SystemUpdateBootstrapStatus( + $this->HookSystem, + $this->SystemTable, + $this->SystemListReset); + } + + /** + * @return SystemList + * + * @see DrupalComponentContainer::SystemList + */ + protected function getSystemList() { + return new SystemList( + $this->Cache, + $this->SystemTable, + $this->DrupalGetFilename, + $this->DrupalStatic); + } + + /** + * @return LibrariesInfo + * + * @see DrupalComponentContainer::LibrariesInfo + */ + protected function getLibrariesInfo() { + return new LibrariesInfo( + $this->DrupalStatic, + $this->HookSystem); + } + + /** + * @return LibrariesLoad + * + * @see DrupalComponentContainer::LibrariesLoad + */ + protected function getLibrariesLoad() { + return new LibrariesLoad( + $this->DrupalStatic, + $this->Cache, + $this->LibrariesInfo); + } + + /** + * @return DrupalBootstrap + * + * @see DrupalComponentContainer::DrupalBoot + */ + protected function getDrupalBoot() { + return new DrupalBootstrap( + $this->DrupalLoad, + $this->HookSystem, + $this->ModuleList); + } + + /** + * @return MockDrupalSystem + * + * @see DrupalComponentContainer::MockDrupalSystem + */ + protected function getMockDrupalSystem() { + return new MockDrupalSystem($this); + } + + /** + * @return DrupalLoad + * + * @see DrupalComponentContainer::DrupalLoad + */ + protected function getDrupalLoad() { + return new DrupalLoad( + $this->DrupalGetFilename); + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalEnvironment.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalEnvironment.php new file mode 100644 index 000000000..bafef37a2 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalEnvironment.php @@ -0,0 +1,106 @@ +components = new DrupalComponentContainer($exampleModules); + $this->exampleModules = $exampleModules; + } + + function setStaticInstance() { + self::$staticInstance = $this; + } + + /** + * @return DrupalEnvironment + */ + static function getInstance() { + return self::$staticInstance; + } + + /** + * @return MockDrupalSystem + */ + function getMockDrupalSystem() { + return $this->components->MockDrupalSystem; + } + + /** + * @return Cache + */ + function getCache() { + return $this->components->Cache; + } + + /** + * @return SystemTable + */ + function getSystemTable() { + return $this->components->SystemTable; + } + + /** + * Simulates Drupal's \module_enable() + * + * @param string[] $module_list + * Array of module names. + * @param bool $enable_dependencies + * TRUE, if dependencies should be enabled too. + * + * @return bool + */ + function moduleEnable(array $module_list, $enable_dependencies = TRUE) { + $this->components->ModuleEnable->moduleEnable($module_list, $enable_dependencies); + } + + /** + * Replicates the Drupal bootstrap. + */ + public function boot() { + $this->components->DrupalBoot->boot(); + } + + /** + * Version of systemUpdateBootstrapStatus() with no side effects. + * + * @see _system_update_bootstrap_status() + */ + public function initBootstrapStatus() { + $bootstrap_modules = $this->exampleModules->getBootstrapModules(); + $this->components->SystemTable->setBootstrapModules($bootstrap_modules); + } + + /** + * @param string $name + * + * @return mixed + */ + public function librariesLoad($name) { + return $this->components->LibrariesLoad->librariesLoad($name); + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalGetFilename.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalGetFilename.php new file mode 100644 index 000000000..d50039a27 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalGetFilename.php @@ -0,0 +1,97 @@ +systemTable = $systemTable; + $this->exampleModules = $exampleModules; + } + + /** + * Replicates drupal_get_filename(*, *, $filename) + * + * @param string $type + * @param string $name + * @param string $filename + */ + function drupalSetFilename($type, $name, $filename) { + if (file_exists($filename)) { + $this->files[$type][$name] = $filename; + } + } + + /** + * Replicates drupal_get_filename(*, *, NULL) + * + * @param string $type + * @param string $name + * + * @return string|null + */ + function drupalGetFilename($type, $name) { + + // Profiles are a special case: they have a fixed location and naming. + if ($type == 'profile') { + $profile_filename = "profiles/$name/$name.profile"; + $this->files[$type][$name] = file_exists($profile_filename) + ? $profile_filename + : FALSE; + } + + // Look in runtime cache. + if (isset($this->files[$type][$name])) { + return $this->files[$type][$name]; + } + + // Load from the database. + $file = $this->systemTable->moduleGetFilename($name); + if (isset($file) && file_exists($file)) { + $this->files[$type][$name] = $file; + return $file; + } + + // Fallback: Search the filesystem. + $this->files[$type] = $this->exampleModules->discoverModuleFilenames($type); + + if (isset($this->files[$type][$name])) { + return $this->files[$type][$name]; + } + + return NULL; + } + + /** + * @see drupal_get_path() + * + * @param string $type + * @param string $name + * + * @return string|null + */ + function drupalGetPath($type, $name) { + return dirname($this->drupalGetFilename($type, $name)); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalLoad.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalLoad.php new file mode 100644 index 000000000..f6e857bc2 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalLoad.php @@ -0,0 +1,46 @@ +drupalGetFilename = $drupalGetFilename; + } + + /** + * @see drupal_load() + */ + function drupalLoad($type, $name) { + + if (isset($this->files[$type][$name])) { + return TRUE; + } + + $filename = $this->drupalGetFilename->drupalGetFilename($type, $name); + + if ($filename) { + include_once $filename; + $this->files[$type][$name] = TRUE; + + return TRUE; + } + + return FALSE; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalStatic.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalStatic.php new file mode 100644 index 000000000..1747d7a0f --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/DrupalStatic.php @@ -0,0 +1,88 @@ +data[$name]) || array_key_exists($name, $this->data)) { + // Non-NULL $name and both $this->data[$name] and $this->default[$name] statics exist. + return $this->data[$name]; + } + // Neither $this->data[$name] nor $this->default[$name] static variables exist. + // First call with new non-NULL $name. Initialize a new static variable. + $this->default[$name] = $this->data[$name] = $default_value; + return $this->data[$name]; + } + + /** + * Replicates drupal_static($name, NULL, TRUE). + * + * @see drupal_static() + * + * @param string $name + * + * @return array + */ + public function &resetKey($name) { + if (!isset($name)) { + throw new \InvalidArgumentException('$name cannot be NULL.'); + } + // First check if dealing with a previously defined static variable. + if (isset($this->data[$name]) || array_key_exists($name, $this->data)) { + // Non-NULL $name and both $this->data[$name] and $this->default[$name] statics exist. + // Reset pre-existing static variable to its default value. + $this->data[$name] = $this->default[$name]; + return $this->data[$name]; + } + // Neither $this->data[$name] nor $this->default[$name] static variables exist. + // Reset was called before a default is set and yet a variable must be + // returned. + return $this->data; + } + + /** + * Replicates drupal_static(NULL, NULL, TRUE). + * + * @see drupal_static() + * + * @return array + */ + public function &resetAll() { + // Reset all: ($name == NULL). This needs to be done one at a time so that + // references returned by earlier invocations of drupal_static() also get + // reset. + foreach ($this->default as $name => $value) { + $this->data[$name] = $value; + } + // As the function returns a reference, the return should always be a + // variable. + return $this->data; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ExampleModulesInterface.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ExampleModulesInterface.php new file mode 100644 index 000000000..21b2b9e67 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ExampleModulesInterface.php @@ -0,0 +1,48 @@ + (object)array( + * 'uri' => 'sites/all/modules/contrib/devel/devel.module', + * 'filename' => 'devel.module', + * 'name' => 'devel', + * )); + */ + public function drupalSystemListingModules(); + + /** + * Replicates drupal_parse_info_file(dirname($module->uri) . '/' . $module->name . '.info') + * + * @see drupal_parse_info_file() + * + * @param string $name + * + * @return array + * Parsed info file contents. + */ + public function drupalParseInfoFile($name); + + /** + * @return true[] + */ + public function getBootstrapModules(); +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/HookSystem.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/HookSystem.php new file mode 100644 index 000000000..7a0d74097 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/HookSystem.php @@ -0,0 +1,71 @@ +moduleImplements = new ModuleImplements($drupalStatic, $cache, $moduleList, $this); + } + + /** + * @param string $hook + */ + function moduleInvokeAll($hook) { + $args = func_get_args(); + assert($hook === array_shift($args)); + foreach ($this->moduleImplements($hook) as $extension) { + $function = $extension . '_' . $hook; + if (function_exists($function)) { + call_user_func_array($function, $args); + } + } + } + + /** + * @param string $hook + * @param mixed $data + */ + function drupalAlter($hook, &$data) { + $args = func_get_args(); + assert($hook === array_shift($args)); + assert($data === array_shift($args)); + while (count($args) < 3) { + $args[] = NULL; + } + foreach ($this->moduleImplements($hook . '_alter') as $extension) { + $function = $extension . '_' . $hook . '_alter'; + $function($data, $args[0], $args[1], $args[2]); + } + } + + /** + * @param string $hook + * + * @throws \Exception + * @return array + */ + function moduleImplements($hook) { + return $this->moduleImplements->moduleImplements($hook); + } + + /** + * Resets the module_implements() cache. + */ + public function moduleImplementsReset() { + $this->moduleImplements->reset(); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesInfo.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesInfo.php new file mode 100644 index 000000000..9aa0d6100 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesInfo.php @@ -0,0 +1,198 @@ +drupalStatic = $drupalStatic; + $this->hookSystem = $hookSystem; + } + + /** + * @see libraries_info() + * + * @param string|null $name + * + * @return mixed + */ + function &getLibrariesInfo($name = NULL) { + // This static cache is re-used by libraries_detect() to save memory. + $libraries = &$this->drupalStatic->get('libraries_info'); + + if (!isset($libraries)) { + $libraries = array(); + // Gather information from hook_libraries_info(). + foreach ($this->hookSystem->moduleImplements('libraries_info') as $module) { + foreach (PureFunctions::moduleInvoke($module, 'libraries_info') as $machine_name => $properties) { + $properties['module'] = $module; + $libraries[$machine_name] = $properties; + } + } + + // Gather information from hook_libraries_info() in enabled themes. + // @see drupal_alter() + // SKIPPED + + // Gather information from .info files. + // .info files override module definitions. + // SKIPPED + + // Provide defaults. + foreach ($libraries as $machine_name => &$properties) { + $this->librariesInfoDefaults($properties, $machine_name); + } + + // Allow modules to alter the registered libraries. + $this->hookSystem->drupalAlter('libraries_info', $libraries); + + // Invoke callbacks in the 'info' group. + // SKIPPED + } + + if (isset($name)) { + if (!empty($libraries[$name])) { + return $libraries[$name]; + } + else { + $false = FALSE; + return $false; + } + } + + return $libraries; + } + + /** + * @see libraries_info_defaults() + * + * @param array $library + * @param string $name + * + * @return array + */ + private function librariesInfoDefaults(&$library, $name) { + $library += array( + 'machine name' => $name, + 'name' => $name, + 'vendor url' => '', + 'download url' => '', + 'path' => '', + 'library path' => NULL, + 'version callback' => 'libraries_get_version', + 'version arguments' => array(), + 'files' => array(), + 'dependencies' => array(), + 'variants' => array(), + 'versions' => array(), + 'integration files' => array(), + 'callbacks' => array(), + ); + $library['callbacks'] += array( + 'info' => array(), + 'pre-detect' => array(), + 'post-detect' => array(), + 'pre-dependencies-load' => array(), + 'pre-load' => array(), + 'post-load' => array(), + ); + + // Add our own callbacks before any others. + array_unshift($library['callbacks']['info'], 'libraries_prepare_files'); + array_unshift($library['callbacks']['post-detect'], 'libraries_detect_dependencies'); + + return $library; + } + + /** + * @see libraries_get_path() + * + * @param string $name + * @param string|bool $base_path + * + * @return string|bool + */ + public function librariesGetPath($name, $base_path = FALSE) { + $libraries = &$this->drupalStatic->get('libraries_get_path'); + + if (!isset($libraries)) { + $libraries = $this->librariesGetLibraries(); + } + + $path = ($base_path ? base_path() : ''); + if (!isset($libraries[$name])) { + return FALSE; + } + else { + $path .= $libraries[$name]; + } + + return $path; + } + + /** + * @see libraries_get_libraries() + */ + private function librariesGetLibraries() { + $searchdir = array(); + # $profile = drupal_get_path('profile', drupal_get_profile()); + # $config = conf_path(); + + // Similar to 'modules' and 'themes' directories in the root directory, + // certain distributions may want to place libraries into a 'libraries' + // directory in Drupal's root directory. + # $searchdir[] = 'libraries'; + + // Similar to 'modules' and 'themes' directories inside an installation + // profile, installation profiles may want to place libraries into a + // 'libraries' directory. + # $searchdir[] = "$profile/libraries"; + + // Always search sites/all/libraries. + # $searchdir[] = 'sites/all/libraries'; + + // Also search sites//*. + # $searchdir[] = "$config/libraries"; + + // Custom location to search + $searchdir[] = dirname(dirname(__DIR__)) . '/fixtures/.libraries'; + + // Retrieve list of directories. + $directories = array(); + $nomask = array('CVS'); + foreach ($searchdir as $dir) { + if (is_dir($dir) && $handle = opendir($dir)) { + while (FALSE !== ($file = readdir($handle))) { + if (!in_array($file, $nomask) && $file[0] != '.') { + if (is_dir("$dir/$file")) { + $directories[$file] = "$dir/$file"; + } + } + } + closedir($handle); + } + } + + return $directories; + } + + public function resetLibrariesInfo() { + $this->drupalStatic->resetKey('libraries_info'); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesLoad.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesLoad.php new file mode 100644 index 000000000..d4a5ee891 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/LibrariesLoad.php @@ -0,0 +1,337 @@ +drupalStatic = $drupalStatic; + $this->cache = $cache; + $this->librariesInfo = $librariesInfo; + } + + /** + * @param string $name + * + * @see libraries_load() + */ + function librariesLoad($name) { + $loaded = &$this->drupalStatic->get('libraries_load', array()); + + if (!isset($loaded[$name])) { + $library = $this->cache->cacheGet($name, 'cache_libraries'); + if ($library) { + $library = $library->data; + } + else { + $library = $this->librariesDetect($name); + $this->cache->cacheSet($name, $library, 'cache_libraries'); + } + + // If a variant was specified, override the top-level properties with the + // variant properties. + if (isset($variant)) { + // Ensure that the $variant key exists, and if it does not, set its + // 'installed' property to FALSE by default. This will prevent the loading + // of the library files below. + $library['variants'] += array($variant => array('installed' => FALSE)); + $library = array_merge($library, $library['variants'][$variant]); + } + // Regardless of whether a specific variant was requested or not, there can + // only be one variant of a library within a single request. + unset($library['variants']); + + // Invoke callbacks in the 'pre-dependencies-load' group. + $this->librariesInvoke('pre-dependencies-load', $library); + + // If the library (variant) is installed, load it. + $library['loaded'] = FALSE; + if ($library['installed']) { + // Load library dependencies. + if (isset($library['dependencies'])) { + foreach ($library['dependencies'] as $dependency) { + $this->librariesLoad($dependency); + } + } + + // Invoke callbacks in the 'pre-load' group. + $this->librariesInvoke('pre-load', $library); + + // Load all the files associated with the library. + $library['loaded'] = $this->librariesLoadFiles($library); + + // Invoke callbacks in the 'post-load' group. + $this->librariesInvoke('post-load', $library); + } + $loaded[$name] = $library; + } + + return $loaded[$name]; + } + + /** + * Tries to detect a library and its installed version. + * + * @param $name + * The machine name of a library to return registered information for. + * + * @return array|false + * An associative array containing registered information for the library + * specified by $name, or FALSE if the library $name is not registered. + * In addition to the keys returned by libraries_info(), the following keys + * are contained: + * - installed: A boolean indicating whether the library is installed. Note + * that not only the top-level library, but also each variant contains this + * key. + * - version: If the version could be detected, the full version string. + * - error: If an error occurred during library detection, one of the + * following error statuses: "not found", "not detected", "not supported". + * - error message: If an error occurred during library detection, a detailed + * error message. + * + * @see libraries_info() + * @see libraries_detect() + */ + private function librariesDetect($name) { + // Re-use the statically cached value of libraries_info() to save memory. + $library = & $this->librariesInfo->getLibrariesInfo($name); + + if ($library === FALSE) { + return $library; + } + // If 'installed' is set, library detection ran already. + if (isset($library['installed'])) { + return $library; + } + + $library['installed'] = FALSE; + + // Check whether the library exists. + if (!isset($library['library path'])) { + $library['library path'] = $this->librariesInfo->librariesGetPath($library['machine name']); + } + if ($library['library path'] === FALSE || !file_exists($library['library path'])) { + $library['error'] = 'not found'; + $library['error message'] = t( + 'The %library library could not be found.', array( + '%library' => $library['name'], + ) + ); + + return $library; + } + + // Invoke callbacks in the 'pre-detect' group. + $this->librariesInvoke('pre-detect', $library); + + // Detect library version, if not hardcoded. + if (!isset($library['version'])) { + // We support both a single parameter, which is an associative array, and an + // indexed array of multiple parameters. + if (isset($library['version arguments'][0])) { + // Add the library as the first argument. + $library['version'] = call_user_func_array($library['version callback'], array_merge(array($library), $library['version arguments'])); + } + elseif ('libraries_get_version' === $library['version callback']) { + $library['version'] = $this->librariesGetVersion($library, $library['version arguments']); + } + else { + $library['version'] = $library['version callback']($library, $library['version arguments']); + } + if (empty($library['version'])) { + $library['error'] = 'not detected'; + $library['error message'] = t( + 'The version of the %library library could not be detected.', array( + '%library' => $library['name'], + ) + ); + + return $library; + } + } + + // Determine to which supported version the installed version maps. + if (!empty($library['versions'])) { + ksort($library['versions']); + $version = 0; + foreach ($library['versions'] as $supported_version => $version_properties) { + if (version_compare($library['version'], $supported_version, '>=')) { + $version = $supported_version; + } + } + if (!$version) { + $library['error'] = 'not supported'; + $library['error message'] = t( + 'The installed version %version of the %library library is not supported.', array( + '%version' => $library['version'], + '%library' => $library['name'], + ) + ); + + return $library; + } + + // Apply version specific definitions and overrides. + $library = array_merge($library, $library['versions'][$version]); + unset($library['versions']); + } + + // Check each variant if it is installed. + if (!empty($library['variants'])) { + foreach ($library['variants'] as $variant_name => &$variant) { + // If no variant callback has been set, assume the variant to be + // installed. + if (!isset($variant['variant callback'])) { + $variant['installed'] = TRUE; + } + else { + // We support both a single parameter, which is an associative array, + // and an indexed array of multiple parameters. + if (isset($variant['variant arguments'][0])) { + // Add the library as the first argument, and the variant name as the second. + $variant['installed'] = call_user_func_array( + $variant['variant callback'], array_merge( + array( + $library, + $variant_name + ), $variant['variant arguments'] + ) + ); + } + else { + $variant['installed'] = $variant['variant callback']($library, $variant_name, $variant['variant arguments']); + } + if (!$variant['installed']) { + $variant['error'] = 'not found'; + $variant['error message'] = t( + 'The %variant variant of the %library library could not be found.', array( + '%variant' => $variant_name, + '%library' => $library['name'], + ) + ); + } + } + } + } + + // If we end up here, the library should be usable. + $library['installed'] = TRUE; + + // Invoke callbacks in the 'post-detect' group. + $this->librariesInvoke('post-detect', $library); + + return $library; + } + + /** + * Invokes library callbacks. + * + * @param string $group + * A string containing the group of callbacks that is to be applied. Should be + * either 'info', 'pre-detect', 'post-detect', or 'load'. + * @param array $library + * An array of library information, passed by reference. + * + * @see libraries_invoke() + */ + private function librariesInvoke($group, &$library) { + foreach ($library['callbacks'][$group] as $callback) { + if ('libraries_detect_dependencies' === $callback) { + continue; + } + $this->librariesTraverseLibrary($library, $callback); + } + } + + /** + * Helper function to apply a callback to all parts of a library. + * + * Because library declarations can include variants and versions, and those + * version declarations can in turn include variants, modifying e.g. the 'files' + * property everywhere it is declared can be quite cumbersome, in which case + * this helper function is useful. + * + * @param array $library + * An array of library information, passed by reference. + * @param callback $callback + * A string containing the callback to apply to all parts of a library. + * + * @see libraries_traverse_library() + */ + private function librariesTraverseLibrary(&$library, $callback) { + // Always apply the callback to the top-level library. + $callback($library, NULL, NULL); + + // Apply the callback to versions. + if (isset($library['versions'])) { + foreach ($library['versions'] as $version_string => &$version) { + $callback($version, $version_string, NULL); + // Versions can include variants as well. + if (isset($version['variants'])) { + foreach ($version['variants'] as $version_variant_name => &$version_variant) { + $callback($version_variant, $version_string, $version_variant_name); + } + } + } + } + + // Apply the callback to variants. + if (isset($library['variants'])) { + foreach ($library['variants'] as $variant_name => &$variant) { + $callback($variant, NULL, $variant_name); + } + } + } + + /** + * Loads a library's files. + * + * @param array $library + * An array of library information as returned by libraries_info(). + * + * @return int + * The number of loaded files. + * + * @see libraries_load_files() + */ + private function librariesLoadFiles($library) { + // Not doing anything here, since library files are not relevant for + // xautoload. + return 0; + } + + /** + * @param $library + * @param $options + * + * @return string + * + * @see libraries_get_version() + */ + private function librariesGetVersion($library, $options) { + return '1.0.0'; + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleBuildDependencies.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleBuildDependencies.php new file mode 100644 index 000000000..9510d3ea2 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleBuildDependencies.php @@ -0,0 +1,190 @@ +name]['edges'] = array(); + if (isset($file->info['dependencies']) && is_array($file->info['dependencies'])) { + foreach ($file->info['dependencies'] as $dependency) { + $dependency_data = $this->drupalParseDependency($dependency); + $graph[$file->name]['edges'][$dependency_data['name']] = $dependency_data; + } + } + } + $this->drupalDepthFirstSearch($graph); + foreach ($graph as $module => $data) { + $files[$module]->required_by = isset($data['reverse_paths']) + ? $data['reverse_paths'] + : array(); + $files[$module]->requires = isset($data['paths']) + ? $data['paths'] + : array(); + $files[$module]->sort = $data['weight']; + } + return $files; + } + + /** + * @see drupal_depth_first_search() + * + * @param $graph + */ + private function drupalDepthFirstSearch(&$graph) { + $state = array( + // The order of last visit of the depth first search. This is the reverse + // of the topological order if the graph is acyclic. + 'last_visit_order' => array(), + // The components of the graph. + 'components' => array(), + ); + // Perform the actual search. + foreach ($graph as $start => $data) { + $this->drupalDepthFirstSearchRec($graph, $state, $start); + } + + // We do such a numbering that every component starts with 0. This is useful + // for module installs as we can install every 0 weighted module in one + // request, and then every 1 weighted etc. + $component_weights = array(); + + foreach ($state['last_visit_order'] as $vertex) { + $component = $graph[$vertex]['component']; + if (!isset($component_weights[$component])) { + $component_weights[$component] = 0; + } + $graph[$vertex]['weight'] = $component_weights[$component]--; + } + } + + /** + * Performs a depth-first search on a graph. + * + * @see _drupal_depth_first_search() + * + * @param array $graph + * A three dimensional associated graph array. + * @param array $state + * An associative array. The key 'last_visit_order' stores a list of the + * vertices visited. The key components stores list of vertices belonging + * to the same the component. + * @param string $start + * An arbitrary vertex where we started traversing the graph. + * @param $component + * The component of the last vertex. + */ + function drupalDepthFirstSearchRec(&$graph, &$state, $start, &$component = NULL) { + // Assign new component for each new vertex, i.e. when not called recursively. + if (!isset($component)) { + $component = $start; + } + // Nothing to do, if we already visited this vertex. + if (isset($graph[$start]['paths'])) { + return; + } + // Mark $start as visited. + $graph[$start]['paths'] = array(); + + // Assign $start to the current component. + $graph[$start]['component'] = $component; + $state['components'][$component][] = $start; + + // Visit edges of $start. + if (isset($graph[$start]['edges'])) { + foreach ($graph[$start]['edges'] as $end => $v) { + // Mark that $start can reach $end. + $graph[$start]['paths'][$end] = $v; + + if (isset($graph[$end]['component']) && $component != $graph[$end]['component']) { + // This vertex already has a component, use that from now on and + // reassign all the previously explored vertices. + $new_component = $graph[$end]['component']; + foreach ($state['components'][$component] as $vertex) { + $graph[$vertex]['component'] = $new_component; + $state['components'][$new_component][] = $vertex; + } + unset($state['components'][$component]); + $component = $new_component; + } + // Only visit existing vertices. + if (isset($graph[$end])) { + // Visit the connected vertex. + $this->drupalDepthFirstSearchRec($graph, $state, $end, $component); + + // All vertices reachable by $end are also reachable by $start. + $graph[$start]['paths'] += $graph[$end]['paths']; + } + } + } + + // Now that any other subgraph has been explored, add $start to all reverse + // paths. + foreach ($graph[$start]['paths'] as $end => $v) { + if (isset($graph[$end])) { + $graph[$end]['reverse_paths'][$start] = $v; + } + } + + // Record the order of the last visit. This is the reverse of the + // topological order if the graph is acyclic. + $state['last_visit_order'][] = $start; + } + + /** + * @see drupal_parse_dependency() + * + * @param $dependency + * + * @return array + */ + private function drupalParseDependency($dependency) { + // We use named subpatterns and support every op that version_compare + // supports. Also, op is optional and defaults to equals. + $p_op = '(?P!=|==|=|<|<=|>|>=|<>)?'; + // Core version is always optional: 7.x-2.x and 2.x is treated the same. + $p_core = '(?:' . preg_quote('7.x') . '-)?'; + $p_major = '(?P\d+)'; + // By setting the minor version to x, branches can be matched. + $p_minor = '(?P(?:\d+|x)(?:-[A-Za-z]+\d+)?)'; + $value = array(); + $parts = explode('(', $dependency, 2); + $value['name'] = trim($parts[0]); + if (isset($parts[1])) { + $value['original_version'] = ' (' . $parts[1]; + foreach (explode(',', $parts[1]) as $version) { + if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) { + $op = !empty($matches['operation']) ? $matches['operation'] : '='; + if ($matches['minor'] == 'x') { + // Drupal considers "2.x" to mean any version that begins with + // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(), + // on the other hand, treats "x" as a string; so to + // version_compare(), "2.x" is considered less than 2.0. This + // means that >=2.x and <2.x are handled by version_compare() + // as we need, but > and <= are not. + if ($op == '>' || $op == '<=') { + $matches['major']++; + } + // Equivalence can be checked by adding two restrictions. + if ($op == '=' || $op == '==') { + $value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x'); + $op = '>='; + } + } + $value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']); + } + } + } + return $value; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleEnable.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleEnable.php new file mode 100644 index 000000000..ba7e4cbc4 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleEnable.php @@ -0,0 +1,225 @@ +drupalGetFilename = $drupalGetFilename; + $this->hookSystem = $hookSystem; + $this->moduleList = $moduleList; + $this->systemTable = $systemTable; + $this->systemListReset = $systemListReset; + $this->systemRebuildModuleData = $systemRebuildModuleData; + $this->systemUpdateBootstrapStatus = $systemUpdateBootstrapStatus; + } + + /** + * Simulates Drupal's module_enable() + * + * @see module_enable() + * + * @param string[] $module_list + * Array of module names. + * @param bool $enable_dependencies + * TRUE, if dependencies should be enabled too. + * + * @return bool + */ + function moduleEnable(array $module_list, $enable_dependencies = TRUE) { + + if ($enable_dependencies) { + $module_list = $this->moduleEnableCheckDependencies($module_list); + } + + if (empty($module_list)) { + // Nothing to do. All modules already enabled. + return TRUE; + } + + $modules_installed = array(); + $modules_enabled = array(); + foreach ($module_list as $module) { + if (1 == $this->systemTable->moduleGetStatus($module)) { + // Already installed + enabled, do nothing. + continue; + } + if (-1 == $this->systemTable->moduleGetSchemaVersion($module)) { + // Install this module. + $this->enableModule($module, TRUE); + $modules_installed[] = $module; + $modules_enabled[] = $module; + } + else { + // Enable the module. + $this->enableModule($module, FALSE); + $modules_enabled[] = $module; + } + } + + // If any modules were newly installed, invoke hook_modules_installed(). + if (!empty($modules_installed)) { + $this->hookSystem->moduleInvokeAll('modules_installed', $modules_installed); + } + + // If any modules were newly enabled, invoke hook_modules_enabled(). + if (!empty($modules_enabled)) { + $this->hookSystem->moduleInvokeAll('modules_enabled', $modules_enabled); + } + + return TRUE; + } + + /** + * @param string[] $module_list + * + * @return string[] + * Module list with added dependencies, sorted by dependency. + * + * @throws \Exception + */ + protected function moduleEnableCheckDependencies(array $module_list) { + // Get all module data so we can find dependencies and sort. + $module_data = $this->systemRebuildModuleData->systemRebuildModuleData(); + // Create an associative array with weights as values. + $module_list = array_flip(array_values($module_list)); + + // The array is iterated over manually (instead of using a foreach) because + // modules may be added to the list within the loop and we need to process + // them. + while ($module = key($module_list)) { + next($module_list); + if (!isset($module_data[$module])) { + // This module is not found in the filesystem, abort. + throw new \Exception("Module '$module' not found."); + } + if ($module_data[$module]->status) { + // Skip already enabled modules. + unset($module_list[$module]); + continue; + } + $module_list[$module] = $module_data[$module]->sort; + + // Add dependencies to the list, with a placeholder weight. + // The new modules will be processed as the while loop continues. + foreach (array_keys($module_data[$module]->requires) as $dependency) { + if (!isset($module_list[$dependency])) { + $module_list[$dependency] = 0; + } + } + } + + if (!$module_list) { + // Nothing to do. All modules already enabled. + return array(); + } + + // Sort the module list by pre-calculated weights. + arsort($module_list); + return array_keys($module_list); + } + + /** + * @param string $extension + * @param bool $install + * + * @see module_enable() + */ + private function enableModule($extension, $install) { + + $filename = $this->drupalGetFilename->drupalGetFilename('module', $extension); + + // Include module files. + require_once $filename; + if (file_exists($install_file = dirname($filename) . '/' . $extension . '.install')) { + require_once $install_file; + } + + // Update status in system table + $this->systemTable->moduleSetEnabled($extension); + + // Clear various caches, especially hook_module_implements() + $this->systemListReset->systemListReset(); + $this->moduleList->moduleList(TRUE); + $this->hookSystem->moduleImplementsReset(); + $this->systemUpdateBootstrapStatus->systemUpdateBootstrapStatus(); + + // Update the registry to include it. + # registry_update(); + // Refresh the schema to include it. + # drupal_get_schema(NULL, TRUE); + // Update the theme registry to include it. + # drupal_theme_rebuild(); + // Clear entity cache. + # entity_info_cache_clear(); + + if ($install) { + PureFunctions::moduleInvoke($extension, 'schema'); + $this->systemTable->moduleSetSchemaVersion($extension, 7000); + PureFunctions::moduleInvoke($extension, 'update_last_removed'); + // Optional hook_install().. + PureFunctions::moduleInvoke($extension, 'install'); + // Optional watchdog() + $this->hookSystem->moduleInvokeAll('watchdog'); + } + // hook_enable() + PureFunctions::moduleInvoke($extension, 'enable'); + // watchdog() + $this->hookSystem->moduleInvokeAll('watchdog'); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleImplements.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleImplements.php new file mode 100644 index 000000000..861614f25 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleImplements.php @@ -0,0 +1,197 @@ +drupalStatic = $drupalStatic; + $this->cache = $cache; + $this->moduleList = $moduleList; + $this->hookSystem = $hookSystem; + } + + /** + * Replicates module_implements(*, *, TRUE) + * + * @see module_implements() + * + * @return null + */ + function reset() { + + // Use the advanced drupal_static() pattern, since this is called very often. + if (!isset($this->drupalStaticFast)) { + $this->drupalStaticFast['implementations'] = &$this->drupalStatic->get('module_implements'); + } + $implementations = &$this->drupalStaticFast['implementations']; + + $implementations = array(); + $this->cache->cacheSet('module_implements', array(), 'cache_bootstrap'); + $this->drupalStatic->resetKey('module_hook_info'); + $this->drupalStatic->resetKey('drupal_alter'); + $this->cache->cacheClearAll('hook_info', 'cache_bootstrap'); + return NULL; + } + + /** + * @see module_implements() + * + * @param string $hook + * @param bool $sort + * + * @return array + */ + function moduleImplements($hook, $sort = FALSE) { + + // Use the advanced drupal_static() pattern, since this is called very often. + if (!isset($this->drupalStaticFast)) { + $this->drupalStaticFast['implementations'] = &$this->drupalStatic->get('module_implements'); + } + $implementations = &$this->drupalStaticFast['implementations']; + + // Fetch implementations from cache. + if (empty($implementations)) { + $cache = $this->cache->cacheGet('module_implements', 'cache_bootstrap'); + if (FALSE === $cache) { + $implementations = array(); + } + else { + $implementations = $cache->data; + } + } + + if (!isset($implementations[$hook])) { + $implementations[$hook] = $this->discoverImplementations($hook, $sort); + } + else { + // @todo Change this when https://drupal.org/node/2263365 has landed in Drupal core. + $this->filterImplementations($implementations[$hook], $hook); + } + + return array_keys($implementations[$hook]); + } + + /** + * @param string $hook + * @param bool $sort + * + * @return array + */ + private function discoverImplementations($hook, $sort) { + + # StaticCallLog::addCall(); + + // The hook is not cached, so ensure that whether or not it has + // implementations, that the cache is updated at the end of the request. + $this->writeCache = TRUE; + $hook_info = $this->moduleHookInfo(); + $implementations = array(); + $list = $this->moduleList->moduleList(FALSE, FALSE, $sort); + + if ('modules_enabled' === $hook) { + # HackyLog::logx($list); + } + + foreach ($list as $module) { + $include_file = isset($hook_info[$hook]['group']) + && module_load_include('inc', $module, $module . '.' . $hook_info[$hook]['group']); + // Since module_hook() may needlessly try to load the include file again, + // function_exists() is used directly here. + if (function_exists($module . '_' . $hook)) { + $implementations[$module] = $include_file + ? $hook_info[$hook]['group'] + : FALSE; + } + } + + // Allow modules to change the weight of specific implementations but avoid + // an infinite loop. + if ($hook != 'module_implements_alter') { + $this->hookSystem->drupalAlter('module_implements', $implementations, $hook); + } + + return $implementations; + } + + /** + * @param array &$implementations + * @param string $hook + */ + private function filterImplementations(&$implementations, $hook) { + foreach ($implementations as $module => $group) { + // If this hook implementation is stored in a lazy-loaded file, so include + // that file first. + if ($group) { + module_load_include('inc', $module, "$module.$group"); + } + // It is possible that a module removed a hook implementation without the + // implementations cache being rebuilt yet, so we check whether the + // function exists on each request to avoid undefined function errors. + // Since module_hook() may needlessly try to load the include file again, + // function_exists() is used directly here. + if (!function_exists($module . '_' . $hook)) { + // Clear out the stale implementation from the cache and force a cache + // refresh to forget about no longer existing hook implementations. + unset($implementations[$module]); + $this->writeCache = TRUE; + } + } + } + + + /** + * Replicates module_hook_info() for some known hooks. + * + * @return array + * An associative array whose keys are hook names and whose values are an + * associative array containing a group name. The structure of the array + * is the same as the return value of hook_hook_info(). + * + * @see hook_hook_info() + */ + private function moduleHookInfo() { + // No core modules implement hook_hook_info(). + return array(); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleList.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleList.php new file mode 100644 index 000000000..b00e89af1 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/ModuleList.php @@ -0,0 +1,124 @@ +drupalGetFilename = $drupalGetFilename; + $this->systemList = $systemList; + $this->drupalStatic = $drupalStatic; + } + + /** + * Replicates module_list(FALSE, FALSE, $sort, $fixed_list) + * + * @param array $fixed_list + * @param bool $sort + * + * @return string[] + */ + function setModuleList($fixed_list, $sort = FALSE) { + + foreach ($fixed_list as $name => $module) { + $this->drupalGetFilename->drupalSetFilename('module', $name, $module['filename']); + $this->list[$name] = $name; + } + + if ($sort) { + return $this->moduleListSorted(); + } + + return $this->list; + } + + /** + * Replicates module_list($refresh, $bootstrap_refresh, $sort, NULL) + * + * @see module_list() + * + * @param bool $refresh + * @param bool $bootstrap_refresh + * @param bool $sort + * + * @return string[] + */ + function moduleList($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE) { + + if (empty($this->list) || $refresh) { + $this->list = array(); + $sorted_list = NULL; + if ($refresh) { + // For the $refresh case, make sure that system_list() returns fresh + // data. + $this->drupalStatic->resetKey('system_list'); + } + if ($bootstrap_refresh) { + $this->list = $this->systemList->systemListBootstrap(); + } + else { + // Not using drupal_map_assoc() here as that requires common.inc. + $this->list = array_keys($this->systemList->systemListModuleEnabled()); + $this->list = !empty($this->list) + ? array_combine($this->list, $this->list) + : array(); + } + } + + if ($sort) { + return $this->moduleListSorted(); + } + + if (count($this->list)) { + # HackyLog::log($this->list); + } + + return $this->list; + } + + /** + * @return string[] + */ + private function moduleListSorted() { + if (!isset($this->moduleListSorted)) { + $this->moduleListSorted = $this->list; + ksort($this->moduleListSorted); + } + return $this->moduleListSorted; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/PureFunctions.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/PureFunctions.php new file mode 100644 index 000000000..bb07c8587 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/PureFunctions.php @@ -0,0 +1,39 @@ +exampleModules = $exampleModules; + $this->hookSystem = $hookSystem; + } + + /** + * Scans and collects module .info data. + * + * @see _system_rebuild_module_data() + * + * @return object[] + */ + public function systemBuildModuleData() { + // Find modules + $modules = $this->exampleModules->drupalSystemListingModules(); + + if (FALSE) { + // Include the installation profile in modules that are loaded. + $profile = 'minimal'; + $modules[$profile] = new \stdClass(); + $modules[$profile]->name = $profile; + $modules[$profile]->uri = 'profiles/' . $profile . '/' . $profile . '.profile'; + $modules[$profile]->filename = $profile . '.profile'; + + // Installation profile hooks are always executed last. + $modules[$profile]->weight = 1000; + } + else { + $profile = 'NO_PROFILE'; + } + + // Set defaults for module info. + $defaults = array( + 'dependencies' => array(), + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + # 'php' => DRUPAL_MINIMUM_PHP, + 'files' => array(), + 'bootstrap' => 0, + ); + + // Read info files for each module. + foreach ($modules as $key => $module) { + // The module system uses the key 'filename' instead of 'uri' so copy the + // value so it will be used by the modules system. + $modules[$key]->filename = $module->uri; + + // Look for the info file. + $module->info = $this->exampleModules->drupalParseInfoFile($module->name); + + // Skip modules that don't provide info. + if (empty($module->info)) { + unset($modules[$key]); + continue; + } + + // Merge in defaults and save. + $modules[$key]->info = $module->info + $defaults; + + // Installation profiles are hidden by default, unless explicitly specified + // otherwise in the .info file. + if ($key == $profile && !isset($modules[$key]->info['hidden'])) { + $modules[$key]->info['hidden'] = TRUE; + } + + // Invoke hook_system_info_alter() to give installed modules a chance to + // modify the data in the .info files if necessary. + $type = 'module'; + $this->hookSystem->drupalAlter('system_info', $modules[$key]->info, $modules[$key], $type); + } + + if (isset($modules[$profile])) { + // The installation profile is required, if it's a valid module. + $modules[$profile]->info['required'] = TRUE; + // Add a default distribution name if the profile did not provide one. This + // matches the default value used in install_profile_info(). + if (!isset($modules[$profile]->info['distribution_name'])) { + $modules[$profile]->info['distribution_name'] = 'Drupal'; + } + } + + unset($modules['NO_PROFILE']); + + return $modules; + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemList.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemList.php new file mode 100644 index 000000000..1814f8eaf --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemList.php @@ -0,0 +1,123 @@ +cache = $cache; + $this->drupalGetFilename = $drupalGetFilename; + $this->systemListLoader = new SystemListLoader($systemTable); + $this->drupalStatic = $drupalStatic; + } + + /** + * Replicates system_list('module_enabled'). + * + * @return object[] + */ + public function systemListModuleEnabled() { + return $this->systemList('module_enabled'); + } + + /** + * Replicates system_list($type), with $type !== 'bootstrap'. + * + * @see system_list() + * + * @param string $type + * Either 'module_enabled', 'theme' or 'filepaths'. + * + * @return object[]|array[] + */ + private function systemList($type) { + $lists = &$this->drupalStatic->get('system_list'); + + if (isset($lists['module_enabled'])) { + return $lists[$type]; + } + + // Otherwise build the list for enabled modules and themes. + if ($cached = $this->cache->cacheGet('system_list', 'cache_bootstrap')) { + $lists = $cached->data; + } + else { + $lists = $this->systemListLoader->loadSystemLists(); + $this->cache->cacheSet('system_list', $lists, 'cache_bootstrap'); + } + // To avoid a separate database lookup for the filepath, prime the + // drupal_get_filename() static cache with all enabled modules and themes. + foreach ($lists['filepaths'] as $item) { + $this->drupalGetFilename->drupalSetFilename($item['type'], $item['name'], $item['filepath']); + } + + return $lists[$type]; + } + + /** + * Replicates system_list('bootstrap') + * + * @see system_list() + * + * @return array|null + */ + function systemListBootstrap() { + $lists = &$this->drupalStatic->get('system_list'); + + // For bootstrap modules, attempt to fetch the list from cache if possible. + // if not fetch only the required information to fire bootstrap hooks + // in case we are going to serve the page from cache. + if (isset($lists['bootstrap'])) { + return $lists['bootstrap']; + } + + if ($cached = $this->cache->cacheGet('bootstrap_modules', 'cache_bootstrap')) { + $bootstrap_list = $cached->data; + } + else { + $bootstrap_list = $this->systemListLoader->fetchBootstrapSystemList(); + $this->cache->cacheSet('bootstrap_modules', $bootstrap_list, 'cache_bootstrap'); + } + + // To avoid a separate database lookup for the filepath, prime the + // drupal_get_filename() static cache for bootstrap modules only. + // The rest is stored separately to keep the bootstrap module cache small. + foreach ($bootstrap_list as $module) { + $this->drupalGetFilename->drupalSetFilename('module', $module->name, $module->filename); + } + + // We only return the module names here since module_list() doesn't need + // the filename itself. + return $lists['bootstrap'] = array_keys($bootstrap_list); + } + +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListLoader.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListLoader.php new file mode 100644 index 000000000..21dead08e --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListLoader.php @@ -0,0 +1,156 @@ +systemTable = $systemTable; + } + + /** + * @return object[] + */ + public function fetchBootstrapSystemList() { + $bootstrapList = array(); + foreach ($this->systemTable->systemTableSortedObjects(NULL, 'module') as $name => $record) { + if (1 == $record->status && 1 == $record->bootstrap) { + $bootstrapList[$name] = (object)array( + 'name' => $record->name, + 'filename' => $record->filename, + ); + } + } + return $bootstrapList; + } + + /** + * @see system_list() + * + * @return array[] + */ + public function loadSystemLists() { + + $lists = array( + 'module_enabled' => array(), + 'theme' => array(), + 'filepaths' => array(), + ); + + // The module name (rather than the filename) is used as the fallback + // weighting in order to guarantee consistent behavior across different + // Drupal installations, which might have modules installed in different + // locations in the file system. The ordering here must also be + // consistent with the one used in module_implements(). + foreach ($this->systemTable->systemTableSortedObjects() as $record) { + // Build a list of all enabled modules. + if ($record->type == 'module') { + if (1 != $record->status) { + continue; + } + $lists['module_enabled'][$record->name] = $record; + } + // Build a list of themes. + elseif ($record->type == 'theme') { + $lists['theme'][$record->name] = $record; + } + else { + continue; + } + // Build a list of filenames so drupal_get_filename can use it. + if ($record->status) { + $lists['filepaths'][] = array( + 'type' => $record->type, + 'name' => $record->name, + 'filepath' => $record->filename + ); + } + } + + $this->themesAddHierarchy($lists['theme']); + + return $lists; + } + + /** + * @param array $themes + */ + private function themesAddHierarchy(array $themes) { + foreach ($themes as $key => $theme) { + if (!empty($theme->info['base theme'])) { + // Make a list of the theme's base themes. + $theme->base_themes = $this->drupalFindBaseThemes($themes, $key); + // Don't proceed if there was a problem with the root base theme. + if (!current($theme->base_themes)) { + continue; + } + // Determine the root base theme. + $base_key = key($theme->base_themes); + // Add to the list of sub-themes for each of the theme's base themes. + foreach (array_keys($theme->base_themes) as $base_theme) { + $themes[$base_theme]->sub_themes[$key] = $theme->info['name']; + } + // Add the base theme's theme engine info. + $theme->info['engine'] = isset($themes[$base_key]->info['engine']) + ? $themes[$base_key]->info['engine'] + : 'theme'; + } + else { + // A plain theme is its own engine. + $base_key = $key; + if (!isset($theme->info['engine'])) { + $theme->info['engine'] = 'theme'; + } + } + // Set the theme engine prefix. + $theme->prefix = ($theme->info['engine'] == 'theme') + ? $base_key + : $theme->info['engine']; + } + } + + /** + * Replicates drupal_find_base_themes() + * + * @param $themes + * @param $key + * @param array $used_keys + * + * @return array + */ + private function drupalFindBaseThemes($themes, $key, $used_keys = array()) { + $base_key = $themes[$key]->info['base theme']; + // Does the base theme exist? + if (!isset($themes[$base_key])) { + return array($base_key => NULL); + } + + $current_base_theme = array($base_key => $themes[$base_key]->info['name']); + + // Is the base theme itself a child of another theme? + if (isset($themes[$base_key]->info['base theme'])) { + // Do we already know the base themes of this theme? + if (isset($themes[$base_key]->base_themes)) { + return $themes[$base_key]->base_themes + $current_base_theme; + } + // Prevent loops. + if (!empty($used_keys[$base_key])) { + return array($base_key => NULL); + } + $used_keys[$base_key] = TRUE; + return $this->drupalFindBaseThemes($themes, $base_key, $used_keys) + $current_base_theme; + } + // If we get here, then this is our parent theme. + return $current_base_theme; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListReset.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListReset.php new file mode 100644 index 000000000..e84497475 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemListReset.php @@ -0,0 +1,39 @@ +cache = $cache; + $this->drupalStatic = $drupalStatic; + } + + /** + * @see system_list_reset() + */ + function systemListReset() { + $this->drupalStatic->resetKey('system_list'); + $this->drupalStatic->resetKey('system_rebuild_module_data'); + $this->drupalStatic->resetKey('list_themes'); + $this->cache->cacheClearAll('bootstrap_modules', 'cache_bootstrap'); + $this->cache->cacheClearAll('system_list', 'cache_bootstrap'); + + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemRebuildModuleData.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemRebuildModuleData.php new file mode 100644 index 000000000..b4633e64f --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemRebuildModuleData.php @@ -0,0 +1,92 @@ +drupalStatic = $drupalStatic; + $this->moduleBuildDependencies = $moduleBuildDependencies; + $this->systemTable = $systemTable; + $this->systemBuildModuleData = $systemBuildModuleData; + $this->systemListReset = $systemListReset; + } + + /** + * Rebuild, save, and return data about all currently available modules. + * + * @see system_rebuild_module_data() + * + * @return array[] + */ + public function systemRebuildModuleData() { + $modules_cache = &$this->drupalStatic->get('system_rebuild_module_data'); + // Only rebuild once per request. $modules and $modules_cache cannot be + // combined into one variable, because the $modules_cache variable is reset by + // reference from system_list_reset() during the rebuild. + if (!isset($modules_cache)) { + $modules = $this->systemBuildModuleData->systemBuildModuleData(); + ksort($modules); + $this->systemTable->systemGetFilesDatabase($modules, 'module'); + $this->systemUpdateFilesDatabase($modules, 'module'); + $modules = $this->moduleBuildDependencies->moduleBuildDependencies($modules); + $modules_cache = $modules; + } + return $modules_cache; + } + + /** + * @see system_update_files_database() + * + * @param object[] $files + * @param string $type + */ + private function systemUpdateFilesDatabase($files, $type) { + $this->systemTable->systemUpdateFilesDatabase($files, $type); + + // If any module or theme was moved to a new location, we need to reset the + // system_list() cache or we will continue to load the old copy, look for + // schema updates in the wrong place, etc. + $this->systemListReset->systemListReset(); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemTable.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemTable.php new file mode 100644 index 000000000..713a0272a --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemTable.php @@ -0,0 +1,330 @@ +addModule($module, array('filename' => $filename)); + } + + /** + * @param string $module + * @param mixed[] $data + * + * @throws \Exception + */ + function addModule($module, $data) { + if (!isset($data['filename'])) { + throw new \Exception("Missing filename in module data."); + } + if ($data['filename'] !== dirname($data['filename']) . '/' . $module . '.module') { + throw new \Exception("Unexpected filename for module."); + } + $this->systemTableData[$module] = $data + array( + 'status' => 0, + 'bootstrap' => 0, + 'schema_version' => -1, + 'weight' => 0, + 'info' => NULL, + 'type' => 'module', + 'name' => $module, + ); + } + + /** + * @param string $module + * @param string $dir + * + * @throws \Exception + */ + function moduleSetDir($module, $dir) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + $filename = $dir . '/' . $module . '.module'; + $this->systemTableData[$module]['filename'] = $filename; + } + + /** + * @param string $module + * @param string $filename + * + * @throws \Exception + */ + function moduleSetFilename($module, $filename) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + $this->systemTableData[$module]['filename'] = $filename; + } + + /** + * @param string $module + * + * @throws \Exception + */ + function moduleSetEnabled($module) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + $this->systemTableData[$module]['status'] = 1; + } + + /** + * @param string $module + * @param int $version + * + * @throws \Exception + */ + public function moduleSetSchemaVersion($module, $version) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + $this->systemTableData[$module]['schema_version'] = $version; + } + + /** + * @return string[] + * Extension type by extension name. + */ + function getActiveExtensions() { + $activeExtensions = array(); + foreach ($this->systemTableData as $module => $data) { + if (1 === $data['status']) { + $activeExtensions[$module] = $data['type']; + } + } + return $activeExtensions; + } + + /** + * Load module data/status from the system table. + * + * @param $module + * + * @return array|null + */ + function moduleGetData($module) { + if (!isset($this->systemTableData[$module])) { + return NULL; + } + return $this->systemTableData[$module]; + } + + /** + * @param string $module + * + * @return string|null + */ + function moduleGetFilename($module) { + if (!isset($this->systemTableData[$module])) { + return NULL; + } + return $this->systemTableData[$module]['filename']; + } + + /** + * @param string $module + * + * @return int + * @throws \Exception + */ + function moduleGetStatus($module) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + return $this->systemTableData[$module]['status']; + } + + /** + * @param string $module + * + * @return int + * @throws \Exception + */ + function moduleGetSchemaVersion($module) { + if (!isset($this->systemTableData[$module])) { + throw new \Exception("Unknown module '$module'."); + } + return $this->systemTableData[$module]['schema_version']; + } + + /** + * @param string[]|null $fields + * @param string|null $type + * + * @return array + */ + public function systemTableObjects(array $fields = NULL, $type = NULL) { + $objects = array(); + foreach ($this->systemTableData as $name => $record) { + if (NULL !== $type && $type !== $record['type']) { + continue; + } + $objects[$name] = $this->makeObject($record, $fields); + } + return $objects; + } + + /** + * @param string[] $fields + * @param string|null $type + * + * @return object[] + */ + public function systemTableSortedObjects(array $fields = NULL, $type = NULL) { + $byWeight = array(); + foreach ($this->systemTableData as $name => $record) { + if (NULL !== $type && $type !== $record['type']) { + continue; + } + $byWeight[$record['weight']][$name] = $this->makeObject($record, $fields); + } + ksort($byWeight); + $sorted = array(); + foreach ($byWeight as $records) { + ksort($records); + $sorted += $records; + } + return $sorted; + } + + /** + * @param $array + * @param array $fields + * + * @return \stdClass + */ + private function makeObject($array, array $fields = NULL) { + if (!isset($fields)) { + return (object)$array; + } + $object = new \stdClass; + foreach ($fields as $field) { + $object->$field = $array[$field]; + } + return $object; + } + + /** + * Retrieves the current status of an array of files in the system table. + * + * @see system_get_files_database() + * + * @param object[] $files + * @param string $type + * E.g. 'module' + */ + public function systemGetFilesDatabase($files, $type) { + $fields = array('filename', 'name', 'type', 'status', 'schema_version', 'weight'); + foreach ($this->systemTableObjects($fields) as $file) { + if ($type !== $file->type) { + continue; + } + if (!isset($files[$file->name]) || !is_object($files[$file->name])) { + continue; + } + $file->uri = $file->filename; + foreach ($file as $key => $value) { + if (!isset($files[$file->name]->$key)) { + $files[$file->name]->$key = $value; + } + } + } + } + + /** + * @see system_update_files_database() + * + * @param object[] $files + * @param string $type + */ + public function systemUpdateFilesDatabase(&$files, $type) { + + // Add all files that need to be deleted to a DatabaseCondition. + foreach ($this->systemTableObjects(NULL, $type) as $record) { + if (isset($files[$record->name]) && is_object($files[$record->name])) { + $file = $files[$record->name]; + // Scan remaining fields to find only the updated values. + foreach ($record as $key => $oldvalue) { + if (isset($file->$key)) { + $this->systemTableData[$record->name][$key] = $file->$key; + } + } + // Indicate that the file exists already. + $file->exists = TRUE; + } + else { + // File is not found in file system, so delete record from the system table. + unset($this->systemTableData[$record->name]); + } + } + + // All remaining files are not in the system table, so we need to add them. + foreach ($files as $name => $file) { + if (isset($file->exists)) { + unset($file->exists); + } + else { + $this->systemTableData[$name] = array( + 'filename' => $file->uri, + 'name' => $file->name, + 'type' => $type, + 'owner' => isset($file->owner) ? $file->owner : '', + 'info' => $file->info, + 'status' => 0, + 'bootstrap' => 0, + 'schema_version' => -1, + 'weight' => 0, + ); + $file->type = $type; + $file->status = 0; + $file->schema_version = -1; + } + } + } + + /** + * @param true[] $bootstrap_modules + */ + public function setBootstrapModules($bootstrap_modules) { + foreach ($this->systemTableData as $name => $record) { + $record['bootstrap'] = empty($bootstrap_modules[$name]) ? 0 : 1; + } + } + + /** + * @param string $name + * @param int $weight + * + * @throws \Exception + */ + public function moduleSetWeight($name, $weight) { + if (!isset($this->systemTableData[$name])) { + throw new \Exception("Unknown module '$name'."); + } + $this->systemTableData[$name]['weight'] = $weight; + } +} diff --git a/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemUpdateBootstrapStatus.php b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemUpdateBootstrapStatus.php new file mode 100644 index 000000000..84dc6d067 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/src/VirtualDrupal/SystemUpdateBootstrapStatus.php @@ -0,0 +1,49 @@ +hookSystem = $hookSystem; + $this->systemTable = $systemTable; + $this->systemListReset = $systemListReset; + } + + /** + * @see _system_update_bootstrap_status() + */ + function systemUpdateBootstrapStatus() { + $bootstrap_modules = array(); + foreach (PureFunctions::bootstrapHooks() as $hook) { + foreach ($this->hookSystem->moduleImplements($hook) as $module) { + $bootstrap_modules[$module] = TRUE; + } + } + $this->systemTable->setBootstrapModules($bootstrap_modules); + // Reset the cached list of bootstrap modules. + $this->systemListReset->systemListReset(); + } +} diff --git a/frontend/drupal/modules/xautoload/tests/test_1/lib/Drupal/xautoload_test_1/ExampleClass.php b/frontend/drupal/modules/xautoload/tests/test_1/lib/Drupal/xautoload_test_1/ExampleClass.php new file mode 100644 index 000000000..3ff9602fd --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/test_1/lib/Drupal/xautoload_test_1/ExampleClass.php @@ -0,0 +1,5 @@ + array( + 'page callback' => '_xautoload_test_1_json', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Page callback for "xautoload-example/json" + */ +function _xautoload_test_1_json() { + $all = EnvironmentSnapshotMaker::getSnapshots('xautoload_test_1'); + drupal_json_output($all); + exit(); +} diff --git a/frontend/drupal/modules/xautoload/tests/test_2/lib/ExampleClass.php b/frontend/drupal/modules/xautoload/tests/test_2/lib/ExampleClass.php new file mode 100644 index 000000000..2f9ff893b --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/test_2/lib/ExampleClass.php @@ -0,0 +1,3 @@ +registerModule(__FILE__); + +/** + * Implements hook_boot() + * + * This turns xautoload_test_2 into a boot module. + */ +function xautoload_test_2_boot() { + _xautoload_test_2_early_boot_observations('boot'); +} + +_xautoload_test_2_early_boot_observations('early'); + +/** + * Test the current state, and remember it. + */ +function _xautoload_test_2_early_boot_observations($phase = NULL) { + EnvironmentSnapshotMaker::takeSnapshot('xautoload_test_2', $phase, array('xautoload_test_2_ExampleClass')); +} + +/** + * Implements hook_menu() + */ +function xautoload_test_2_menu() { + return array( + 'xautoload_test_2.json' => array( + 'page callback' => '_xautoload_test_2_json', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Page callback for "xautoload-example/json" + */ +function _xautoload_test_2_json() { + $all = EnvironmentSnapshotMaker::getSnapshots('xautoload_test_2'); + drupal_json_output($all); + exit(); +} diff --git a/frontend/drupal/modules/xautoload/tests/test_3/lib/ExampleClass.php b/frontend/drupal/modules/xautoload/tests/test_3/lib/ExampleClass.php new file mode 100644 index 000000000..d36ed25b3 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/test_3/lib/ExampleClass.php @@ -0,0 +1,5 @@ +registerModulePsr4(__FILE__, 'lib'); + +/** + * Implements hook_boot() + * + * This turns xautoload_test_2 into a boot module. + */ +function xautoload_test_3_boot() { + _xautoload_test_3_early_boot_observations('boot'); +} + +_xautoload_test_3_early_boot_observations('early'); + +/** + * Test the current state, and remember it. + */ +function _xautoload_test_3_early_boot_observations($phase = NULL) { + EnvironmentSnapshotMaker::takeSnapshot( + 'xautoload_test_3', + $phase, + array('Drupal\xautoload_test_3\ExampleClass')); +} + +/** + * Implements hook_menu() + */ +function xautoload_test_3_menu() { + return array( + 'xautoload_test_3.json' => array( + 'page callback' => '_xautoload_test_3_json', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Page callback for "xautoload-example/json" + */ +function _xautoload_test_3_json() { + $all = EnvironmentSnapshotMaker::getSnapshots('xautoload_test_3'); + drupal_json_output($all); + exit(); +} diff --git a/frontend/drupal/modules/xautoload/tests/test_4/testlib/src/TestClass.php b/frontend/drupal/modules/xautoload/tests/test_4/testlib/src/TestClass.php new file mode 100644 index 000000000..9acaeec8a --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/test_4/testlib/src/TestClass.php @@ -0,0 +1,7 @@ +addPsr4('Drupal\xautoload_test_4\testlib\\', 'testlib/src'); +} + +/** + * Implements hook_menu() + */ +function xautoload_test_4_menu() { + // Let's see if this breaks. + new \Drupal\xautoload_test_4\testlib\TestClass(); +} + +/** + * Implements hook_theme() + */ +function xautoload_test_4_theme() { + new \Drupal\xautoload_test_4\testlib\TestClass(); +} diff --git a/frontend/drupal/modules/xautoload/tests/test_5/src/FooNamespace/Foo_Class.php b/frontend/drupal/modules/xautoload/tests/test_5/src/FooNamespace/Foo_Class.php new file mode 100644 index 000000000..6c6394005 --- /dev/null +++ b/frontend/drupal/modules/xautoload/tests/test_5/src/FooNamespace/Foo_Class.php @@ -0,0 +1,9 @@ +finder->addPsr4('Aaa\Bbb\\', 'sites/all/libraries/aaa-bbb/src'); + + // Or use an adapter with more powerful methods. + xautoload()->adapter->composerDir('sites/all/vendor/composer'); +} + +/** + * Implements hook_xautoload() + * + * Register additional classes, namespaces, autoload patterns, that are not + * already registered by default. + * + * @param \Drupal\xautoload\Adapter\LocalDirectoryAdapter $adapter + * An adapter object that can register stuff into the class loader. + */ +function hook_xautoload($adapter) { + + // Register a namespace with PSR-0. + $adapter->add( + // Namespace of a 3rd party package included in the module directory. + 'Acme\GardenKit\\', + // Path to the 3rd party package, relative to the module directory. + 'shrubbery/lib'); + + // Register a namespace with PSR-4. + $adapter->absolute()->addPsr4( + // The namespace. + 'Acme\ShrubGardens\\', + // Absolute path to the PSR-4 base directory. + '/home/karnouffle/php/shrub-gardens/src'); + + // Scan sites/all/vendor/composer for Composer-generated autoload files, e.g. + // 'sites/all/vendor/composer/autoload_namespaces.php', etc. + $adapter->absolute()->composerDir('sites/all/vendor/composer'); +} + + +/** + * Implements hook_libraries_info() + * + * Allows to register PSR-0 (or other) class folders for your libraries. + * (those things living in sites/all/libraries) + * + * The original documentation for this hook is at libraries module, + * libraries.api.php + * + * X Autoload extends the capabilities of this hook, by adding an "xautoload" + * key. This key takes a callback or closure function, which has the same + * signature as hook_xautoload($adapter). + * This means, you can use the same methods on the $api object. + * + * @return array[] + * Same as explained in libraries module, but with added key 'xautoload'. + */ +function mymodule_libraries_info() { + + return array( + 'ruebenkraut' => array( + 'name' => 'Rübenkraut library', + 'vendor url' => 'http://www.example.com', + 'download url' => 'http://github.com/example/ruebenkraut', + 'version' => '1.0', + 'xautoload' => function($adapter) { + /** + * @var \Drupal\xautoload\Adapter\LocalDirectoryAdapter $adapter + * An adapter object that can register stuff into the class loader. + */ + // Register a namespace with PSR-0 root in + // 'sites/all/libraries/ruebenkraut/src'. + $adapter->add('Rueben\Kraut\\', 'src'); + }, + ), + 'gurkentraum' => array( + 'name' => 'Gurkentraum library', + 'xautoload' => function($adapter) { + /** @var \Drupal\xautoload\Adapter\LocalDirectoryAdapter $adapter */ + // Scan sites/all/libraries/ruebenkraut/composer.json to look for + // autoload information. + $adapter->composerJson('composer.json'); + } + ) + ); +} diff --git a/frontend/drupal/modules/xautoload/xautoload.early.inc b/frontend/drupal/modules/xautoload/xautoload.early.inc new file mode 100644 index 000000000..4882e8b20 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.early.inc @@ -0,0 +1,14 @@ +classFinder; +} + +/** + * Get a service object from the registry. + * Services are lazy-created first time you need them. + * + * @param string $key + * Identifier of the service within the registry. + * The xautoload_ServiceFactory should have a method with the same name. + * The recommended way (esp if you ask your IDE) is to omit this parameter and + * use xautoload()->$key instead. + * + * @return Main|object + */ +function xautoload($key = 'main') { + static $service_registry; + static $main; + if (!isset($service_registry)) { + $service_factory = new ServiceFactory(); + $service_registry = new ServiceContainer($service_factory); + $main = $service_registry->main; + } + switch ($key) { + case 'main': + return $main; + default: + // Legacy.. + return $service_registry->get($key); + } +} + + +// "Private" functions. +// ----------------------------------------------------------------------------- + +/** + * Creates and registers the xautoload class loader. + * + * Registers the xautoload_ prefix and the Drupal\xautoload\\ namespace, but + * does not register any other Drupal-specific stuff yet. This is because this + * might be called from settings.php, while some parts of Drupal are not fully + * initialized yet. + */ +function _xautoload_register() { + + // Check that this runs only once. + static $_first_run = TRUE; + if (!$_first_run) { + return; + } + $_first_run = FALSE; + + // Register a temporary loader. + spl_autoload_register('_xautoload_autoload_temp', TRUE, TRUE); + + // Some classes need to be loaded manually. Believe it! + LoadClassInjectedAPI::forceAutoload(); + LoadClassGetFileInjectedApi::forceAutoload(); + \Drupal\xautoload\Util::forceAutoload(); + + $finder = xautoload()->finder; + $finder->register(); + + // Register the 'Drupal\xautoload\\' namespace. + $finder->addPsr4('Drupal\xautoload\\', XAUTOLOAD_SRC_DIR . '/'); + + // Register the "xautoload_" prefix for legacy classes. + $finder->addPearFlat('xautoload_', __DIR__ . '/legacy/lib/'); + + // Unregister the temporary loader. + spl_autoload_unregister('_xautoload_autoload_temp'); +} + +/** + * Temporary loader callback, to avoid any module_load_include() + * while building the real autoloader. + * + * @param string $name + * Name of the class or interface we want to load. + * + * @throws \Exception + */ +function _xautoload_autoload_temp($name) { + + if ('Drupal\xautoload\\' === substr($name, 0, 17)) { + // PSR-4 case + $file = XAUTOLOAD_SRC_DIR . '/' . str_replace('\\', '/', substr($name, 17)) . '.php'; + require_once $file; + } + elseif ('xautoload_' === substr($name, 0, 10) && FALSE === strpos($name, '\\')) { + // Legacy case + $file = XAUTOLOAD_LIB_DIR . '/' . str_replace('_', '/', substr($name, 10)) . '.php'; + require_once $file; + } +} diff --git a/frontend/drupal/modules/xautoload/xautoload.emulate.inc b/frontend/drupal/modules/xautoload/xautoload.emulate.inc new file mode 100644 index 000000000..4843968b2 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.emulate.inc @@ -0,0 +1,71 @@ +system->installSetModuleWeight(-90); +} + +/** + * Implements hook_uninstall() + */ +function xautoload_uninstall() { + variable_del(XAUTOLOAD_VARNAME_CACHE_TYPES); + variable_del(XAUTOLOAD_VARNAME_CACHE_LAZY); + variable_del(XAUTOLOAD_VARNAME_REPLACE_CORE); + variable_del(XAUTOLOAD_VARNAME_CACHE_PREFIX); + + // The following variable is a leftover from previous versions. + variable_del('xautoload_cache_mode'); +} + +/** + * Implements hook_update_N() + */ +function xautoload_update_7000() { + // Set module weight for xautoload to run before other modules. + db_query("UPDATE {system} SET weight = -90 WHERE name = 'xautoload' AND type = 'module'"); +} diff --git a/frontend/drupal/modules/xautoload/xautoload.libraries.inc b/frontend/drupal/modules/xautoload/xautoload.libraries.inc new file mode 100644 index 000000000..f6a97fb68 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.libraries.inc @@ -0,0 +1,15 @@ +librariesInfoAlter->librariesInfoAlter($info); +} diff --git a/frontend/drupal/modules/xautoload/xautoload.module b/frontend/drupal/modules/xautoload/xautoload.module new file mode 100644 index 000000000..c20584408 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.module @@ -0,0 +1,198 @@ +phaseControl->enterMainPhase(); +} + +/** + * Implements hook_init() + * + * Note: + * This is a first step to allow modules to register foreign namespaces. + * We will probably change this, to allow bootstrap modules to register their + * namespaces earlier in the request. + * We might also find a solution to cache the result of this hook between + * requests. This would require a different implementation of the InjectedAPI, + * which would no longer have a direct reference to the finder object. + */ +function xautoload_init() { + xautoload()->phaseControl->enterMainPhase(); +} + +/** + * Implements hook_system_theme_info(). + * + * This is the first hook to fire on update.php. + * + * Unfortunately, hook_custom_theme() and hook_init() are not called on + * update.php in _drupal_bootstrap_full(). + * + * But in list_themes(), _system_rebuild_theme_data() is always called in + * maintenance mode. And from there, hook_system_theme_info(). + * + * @see _drupal_bootstrap_full() + * @see list_themes() + */ +function xautoload_system_theme_info() { + xautoload()->phaseControl->enterMainPhase(); +} + +/** + * Implements hook_module_implements_alter() + * + * @param array &$implementations + * @param string $hook + */ +function xautoload_module_implements_alter(&$implementations, $hook) { + + // Check if new modules have been enabled. + if ('boot' === $hook) { + + # \Drupal\xautoload\Tests\HackyLog::log($hook); + // hook_module_implements_alter('boot') gets called (indirectly) from + // _system_update_bootstrap_status(), which happens each time a new module + // is enabled. + xautoload()->phaseControl->checkNewExtensions(); + } + + // Most hook implementations are in dedicated files. + switch ($hook) { + case 'init': + case 'custom_theme': + case 'system_theme_info': + // Move xautoload_$hook() to the start. + $implementations = array('xautoload' => FALSE) + $implementations; + break; + case 'form_system_performance_settings_alter': + // Specify that the implementation lives in xautoload.ui.inc. + $implementations['xautoload'] = 'ui'; + require_once __DIR__ . '/xautoload.ui.inc'; + break; + case 'modules_enabled': + case 'registry_files_alter': + // Move xautoload_$hook() to the start, and specify that the + // implementation lives in xautoload.system.inc. + $implementations = array('xautoload' => 'system') + $implementations; + require_once __DIR__ . '/xautoload.system.inc'; + break; + case 'libraries_info_alter': + $implementations['xautoload'] = 'libraries'; + require_once __DIR__ . '/xautoload.libraries.inc'; + break; + default: + return; + } +} + + +// "Private" functions. +// ----------------------------------------------------------------------------- + +/** + * Registers Drupal-related namespaces and prefixes in the xautoload loader, and + * activates the APC (or similar) cache, if enabled. + */ +function _xautoload_register_drupal() { + + // Check that this runs only once. + static $_first_run = TRUE; + if (!$_first_run) { + return; + } + $_first_run = FALSE; + + // Register the class loader itself. + require_once __DIR__ . '/xautoload.early.lib.inc'; + _xautoload_register(); + + $services = xautoload()->getServiceContainer(); + + if ($services->system->variableGet(XAUTOLOAD_VARNAME_REPLACE_CORE)) { + /** + * Completely take over. + * + * @see _drupal_bootstrap_database() + * @see drupal_autoload_class() + * @see drupal_autoload_interface() + */ + spl_autoload_unregister('drupal_autoload_class'); + spl_autoload_unregister('drupal_autoload_interface'); + } + + $lazy = $services->system->variableGet(XAUTOLOAD_VARNAME_CACHE_LAZY, FALSE); + $decorated = $lazy + ? $services->proxyFinder + : $services->proxyFinder->getFinder() + ; + + // Activate a cache, if available and enabled. + $cache_types = $services->system->variableGet(XAUTOLOAD_VARNAME_CACHE_TYPES, array()); + if (!empty($cache_types['apcu_q']) && extension_loaded('apcu') && function_exists('apcu_store')) { + $cached_loader = ApcuQueuedCachedClassLoader::create($decorated, $services->cacheManager); + } + elseif (!empty($cache_types['apc']) && extension_loaded('apc') && function_exists('apc_store')) { + $cached_loader = ApcClassLoader::create($decorated, $services->cacheManager); + } + /** @noinspection NotOptimalIfConditionsInspection */ + elseif (!empty($cache_types['apcu']) && extension_loaded('apcu') && function_exists('apcu_store')) { + $cached_loader = ApcuClassLoader::create($decorated, $services->cacheManager); + } + elseif (!empty($cache_types['wincache']) && extension_loaded('wincache') && function_exists('wincache_ucache_get')) { + $cached_loader = WinCacheClassLoader::create($decorated, $services->cacheManager); + } + elseif (!empty($cache_types['xcache']) && extension_loaded('Xcache') && function_exists('xcache_get')) { + $cached_loader = XCacheClassLoader::create($decorated, $services->cacheManager); + } + elseif (!empty($cache_types['dbcache'])) { + $cached_loader = DbCacheClassLoader::create($decorated, $services->cacheManager); + } + + if (isset($cached_loader)) { + if ($lazy) { + $decorated->observeFirstCacheMiss(new CacheMissLoaderSetFinder($cached_loader)); + } + $cached_loader->register(); + $services->finder->unregister(); + } + else { + // No cache is active. + // Initialize the finder, to fire scheduled operations. + $services->proxyFinder->getFinder(); + } + + // Register prefixes and namespaces for enabled extensions. + $services->proxyFinder->observeFirstCacheMiss($services->phaseControl); +} diff --git a/frontend/drupal/modules/xautoload/xautoload.system.inc b/frontend/drupal/modules/xautoload/xautoload.system.inc new file mode 100644 index 000000000..b37df33c5 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.system.inc @@ -0,0 +1,41 @@ +phaseControl->modulesEnabled($modules); +} + +/** + * Implements hook_registry_files_alter() + * + * Support wildcard syntax in the files[] setting in your module's info file. + * See https://drupal.org/node/1976198 + * + * This function will remove entries like foo/inc/**, and instead add all the + * individual class files found in the foo/inc/ folder. + * + * @param array[] &$files + * List of files specified in files[] array in module info files. + * Format: + * + * $files['modules/field/field.attach.inc'] = array( + * 'module' => 'field', + * 'weight' => 0, + * ); + * // Wildcard syntax. + * $files['sites/all/modules/foo/inc/**'] = array( + * 'module' => 'foo', + * 'weight' => 0, + * ); + */ +function xautoload_registry_files_alter(&$files) { + $file_finder = new WildcardFileFinder(); + $file_finder->addDrupalPaths($files); + $files = $file_finder->getDrupalFiles(); +} diff --git a/frontend/drupal/modules/xautoload/xautoload.ui.inc b/frontend/drupal/modules/xautoload/xautoload.ui.inc new file mode 100644 index 000000000..5f9aca833 --- /dev/null +++ b/frontend/drupal/modules/xautoload/xautoload.ui.inc @@ -0,0 +1,89 @@ + 'fieldset', + '#title' => t('X Autoload'), + ); + + $cache_default_value = variable_get(XAUTOLOAD_VARNAME_CACHE_TYPES, array()); + + $cache_status = array( + 'apcu_q' => $apcu_status = (extension_loaded('apcu') && function_exists('apcu_store')), + 'apc' => (extension_loaded('apc') && function_exists('apc_store')), + 'apcu' => $apcu_status, + 'wincache' => (extension_loaded('WinCache') && function_exists('wincache_ucache_get')), + 'xcache' => (extension_loaded('Xcache') && function_exists('xcache_get')), + 'dbcache' => TRUE, + ); + $cache_names = array( + 'apcu_q' => t('Self-updating APCu classmap (*)'), + 'apc' => 'APC', + 'apcu' => 'APCu', + 'wincache' => 'WinCache', + 'xcache' => 'XCache', + 'dbcache' => t('Self-updating database classmap (*)'), + ); + $options = $cache_names; + $options_descriptions = array(); + $options['dbcache'] = l($options['dbcache'], 'https://www.drupal.org/node/2451261'); + $active_cache_key = NULL; + $active_cache_name = t('No cache.'); + foreach ($cache_names as $key => $title) { + $status = $cache_status[$key]; + if (!isset($active_cache_key) && $status && !empty($cache_default_value[$key])) { + $active_cache_key = $key; + $active_cache_name = $title; + } + } + foreach ($options as $key => $title) { + if ($cache_status[$key]) { + $options[$key] .= ' (' . t('Running and available') . ')'; + } + else { + $options[$key] .= ' (' . t('Not currently available') . ')'; + $options[$key] = '' . $options[$key] . ''; + } + } + + $form['xautoload'][XAUTOLOAD_VARNAME_CACHE_TYPES] = array( + /* @see system_element_info() */ + '#type' => 'checkboxes', + '#title' => t('Cache mode'), + '#default_value' => $cache_default_value, + '#options' => $options, + '#options_descriptions' => $options_descriptions, + '#description' => '' + . '

' . t('X Autoload will pick the first cache mode that is available and enabled.') + . '
' . t('Currently, this is:') . ' ' . $active_cache_name . '.' + . '

' . t('It is usually safe to enable all these checkboxes, so xautoload can always use the best cache mode available on your system.') + . '
' . t('This also makes it easier to sync these settings between environments, where different PHP extensions might be installed.') + . '

' + . '

(*) ' . t('The "Self-updating [_] classmap" cache types require more than one request until they are "hot", but may bring higher performance.') + . '

', + ); + + $form['xautoload'][XAUTOLOAD_VARNAME_CACHE_LAZY] = array( + '#type' => 'checkbox', + '#title' => t('Postpone registration of module namespaces until the first cache miss (recommended).'), + '#default_value' => variable_get(XAUTOLOAD_VARNAME_CACHE_LAZY, FALSE), + '#description' => t('This should speed up the bootstrap of xautoload.'), + ); + + $form['xautoload'][XAUTOLOAD_VARNAME_REPLACE_CORE] = array( + '#type' => 'checkbox', + '#title' => t('Replace core class loader.'), + '#default_value' => variable_get(XAUTOLOAD_VARNAME_REPLACE_CORE, FALSE), + '#description' => t('Lets xautoload replace Drupal\'s drupal_autoload_class() and drupal_autoload_interface().') + . '
' . t('Improves performance, if at least one of the cache options is active and enabled.') + . '
' . t('This features is quite new. Please report any problems in the xautoload issue queue on drupal.org.'), + ); +}